Министерство образования и науки Российской Федерации Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования «Самарский государственный аэрокосмический университет имени академика С.П. Королева (национальный исследовательский университет)» Факультет информатики Кафедра технической кибернетики Выпускная квалификационная работа магистра на тему Применение алгоритмов дискретного преобразования Фурье для решения задачи формирования модели водной поверхности Выпускник _____________________________________ Князев В.А. (подпись) Руководитель работы _____________________________ Чичева М.А. (подпись) Нормоконтролёр _________________________________ Суханов С.В. (подпись) Рецензент ______________________________________ (подпись) САМАРА 2012 Министерство образования и науки Российской Федерации Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования «Самарский государственный аэрокосмический университет имени академика С.П. Королева (национальный исследовательский университет)» Факультет информатики Кафедра технической кибернетики «УТВЕРЖДАЮ» Заведующий кафедрой ____________________________ «___»_______________ 20____ г. ЗАДАНИЕ НА ВЫПУСКНУЮ КВАЛИФИКАЦИОННУЮ РАБОТУ МАГИСТРА студенту М627 группы Князеву Виталию Александровичу 1. Тема работы Применение алгоритмов дискретного преобразования Фурье для решения задачи формирования модели водной поверхности утверждена приказом по университету от «25» апреля 2012 г. № 164-СТ. 2. Исходные данные к работе: Методы и алгоритмы последовательного и параллельного вычисления дискретных преобразований Фурье, подход к моделированию водной поверхности. 3. Перечень вопросов, подлежащих разработке: 2 3.1. Изучение способов построения 3D-модели водной поверхности 3.2 Изучение методов вычисления многомерных дискретных ортогональных преобразований. 3.3. Создание исследовательского программного комплекса. 3.4. Проведение вычислительных экспериментов и анализ результатов. Срок представления законченной работы «___» __________ 20___ г. Руководитель работы ______________________ Чичева М.А. (подпись) Задание принял к исполнению «___» __________ 20___ г. ______________ (подпись) 3 Князев В.А. РЕФЕРАТ Выпускная квалификационная работа магистра: 113 c., 30 рисунков, 15 таблиц, 13 источников, 3 приложения. Презентация: 14 слайдов Microsoft PowerPoint. ДИСКРЕТНОЕ ПРЕОБРАЗОВАНИЕ ФУРЬЕ, 3D-МОДЕЛЬ ВОДНОЙ ПОВЕРХНОСТИ, БЫСТРЫЕ АЛГОРИТМЫ, ПАРАЛЛЕЛЬНЫЕ АЛГОРИТМЫ, ГИПЕРКОМПЛЕКСНАЯ АЛГЕБРА, ПРЯМАЯ СУММА КОМПЛЕКСНЫХ АЛГЕБР, JAVA, CUDA Объектом исследования являются алгоритмы дискретного преобразования Фурье и методы их распараллеливания. Цель работы – исследование и сравнительный анализ параллельных алгоритмов дискретного преобразования Фурье (ДПФ) в задаче построения 3Dмодели водной поверхности. Разработана программная реализация рассмотренных алгоритмов и программного комплекса, строящего 3D-модель водной поверхности. Проведены вычислительные эксперименты по быстродействию алгоритмов преобразования Фурье как многоядерном центральном процессоре (CPU), так и на графическом процессоре (GPU). Произведен сравнительный анализ полученных результатов. 4 СОДЕРЖАНИЕ ОПРЕДЕЛЕНИЯ, ОБОЗНАЧЕНИЯ И СОКРАЩЕНИЯ ......................................... 8 ВВЕДЕНИЕ .................................................................................................................. 9 1 2 Модель водной поверхности океана ................................................................. 11 1.1 Волны Герстнера .......................................................................................... 11 1.2 Статистическая модель ................................................................................ 14 Алгоритмы дискретного преобразования Фурье и способы их распараллеливания .................................................................................................... 18 2.1 Используемые алгебраические структуры................................................. 18 2.1.1 Алгебра гиперкомплексных чисел ....................................................... 18 2.1.2 Прямая сумма комплексных алгебр ..................................................... 22 2.2 Одномерное ДПФ ......................................................................................... 25 2.3 Двумерное ДПФ............................................................................................ 30 2.3.1 Построчно-столбцовый алгоритм ДПФ ............................................. 30 2.3.2 Алгоритм двумерного ДПФ по основанию 2 ...................................... 31 2.3.3 Учет симметрий спектра вещественного сигнала .............................. 35 2.4 Способы распараллеливания ....................................................................... 37 2.4.1 По компонентам в построчно-столбцовом алгоритме ....................... 37 2.4.2 Распараллеливание по структуре декомпозиции для двумерного случая .............................................................................................. 38 2.4.3 Распараллеливание за счет структуры алгебры для двумерного случая .............................................................................................. 39 3 Программная реализация алгоритмов .............................................................. 42 3.1 Описание технологии Cuda ......................................................................... 42 5 3.2 Описание библиотеки JCuda ....................................................................... 46 3.3 Исследовательский программный комплекс формирования модели водной поверхности............................................................................................... 48 3.4 Общие сведения для алгоритмов двумерного случая ............................... 51 3.5 Программная реализация последовательного алгоритма двумерного ДПФ .................................................................................................... 52 3.6 Программная реализация последовательного построчно-столбцового алгоритма двумерного ДПФ ................................................................................. 54 3.7 Программная реализация алгоритма двумерного ДПФ с распараллеливанием за счет структуры алгебры ............................................... 56 3.8 Программная реализация алгоритма двумерного ДПФ с распараллеливанием по структуре декомпозиции ............................................. 58 3.9 Программная реализация гибридного алгоритма двумерного ДПФ с распараллеливанием по структуре декомпозиции и распараллеливанием по структуре алгебры.................................................................................................. 61 3.10 Программная реализация ДПФ с постоянной синхронизацией процессов и без ...................................................................................................... 64 4 Экспериментальные исследования алгоритмов и анализ результатов ......... 68 4.1 Исследование скорости выполнения алгоритмов на CPU ....................... 69 4.2 Исследование скорости выполнения алгоритмов на GPU ....................... 77 4.3 Исследование скорости прорисовки модели водной поверхности ......... 81 ЗАКЛЮЧЕНИЕ ......................................................................................................... 84 СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ ............................................... 86 ПРИЛОЖЕНИЕ А Графики сравнения алгоритмов ДПФ .................................... 88 ПРИЛОЖЕНИЕ Б Примеры работы программы для разных размеров сетки .... 91 6 ПРИЛОЖЕНИЕ В Исходный текст программы .................................................... 93 7 ОПРЕДЕЛЕНИЯ, ОБОЗНАЧЕНИЯ И СОКРАЩЕНИЯ ДПФ – дискретное преобразование Фурье. ГДПФ – дискретное преобразование в алгебре гиперкомплексных чисел. CPU – центральный процессор (англ., Central Processing Unit). GPU – графический процессор (англ., Graphics Processing Unit). FPS – число кадров в секунду (англ., Frames per Second). 8 ВВЕДЕНИЕ Преобразование Фурье является мощным инструментом, применяемым в различных научных областях. Оно используется при изучении колебательных процессов. Преобразование Фурье можно использовать как средство решения сложных уравнений, описывающих динамические процессы, которые возникают под воздействием электрической, тепловой или световой энергии. В других случаях оно позволяет выделять регулярные составляющие в сложном колебательном сигнале, благодаря чему можно правильно интерпретировать экспериментальные наблюдения в астрономии, медицине и химии. Всё это вызывает большой интерес к этому преобразованию. Однако само дискретное преобразование Фурье выполняется очень медленно, и обработка большого входного сигнала занимает долгое время. Поэтому на смену «прямому» преобразованию пришли быстрые алгоритмы, которые уменьшают вычислительную сложность преобразования [1]. К тому же их выполнение можно разбить между несколькими процессорами/компьютерами, добившись тем самым параллельной реализации. Затем, около 20 лет назад, появились первые работы, в которых одним из подходов к синтезу быстрых алгоритмов ДПФ являлось погружение данных в специальные алгебраические структуры, что позволило уменьшить вычислительную сложность, особенно в случае вещественных входных данных [2]. Сфера применения дискретного преобразования Фурье очень широка. Оно играет важную роль в физике плазмы и полупроводниковых материалов, химии, микроволновой акустике, медицинских обследованиях, радиолокации, сейсмологии, океанографии и 3D-моделировании. Построение 3D-модели водной поверхности океана – типичная задача, которая ставится как в научных целях (океанология), так и в сферах кинематографии и разработки интерактивных 3D-приложений и видеоигр, где 9 основными критериями оценки качества модели является её реалистичность и скорость построения. В данной работе проводится исследование быстрых алгоритмов двумерного ДПФ при построении 3D-модели поверхности океана. Были реализованы как «обычные» алгоритмы (построчно-столбцвое преобразование, алгоритм Кули-Тьюки) в пространстве комплексных чисел, так и алгоритмы с погружением входного сигнала в четырехмерную алгебру (алгебра гиперкомплексных чисел, прямая сумма комплексных алгебр). В разделе 1 ставится задача для данной работы, а также приводится описание двух алгоритмов построения модели водной поверхности. Теоретические сведения об алгебраических структурах и описание алгоритмов преобразования Фурье приводятся в разделе 2. В разделе 3 описываются реализации алгоритмов, а также используемые технологии. С помощью реализованной программы были исследованы как скорость выполнения самих алгоритмов, так и скорость построения 3D-модели. Эти результаты приведены в разделе 4. 10 1 Модель водной поверхности океана Вода в компьютерной графике стала распространенным инструментом в области визуальных эффектов: начиная обычными иллюстрациями с заранее отрисованной моделью и заканчивая художественными фильмами, в которых присутствует еще и анимация [3]. Компании, занимающиеся разработкой продуктов в сфере 3D-графики, продолжают расширять и совершенствовать эти инструменты, стремясь к более качественным с точки зрения геометрии формам поверхности, сложным взаимодействиям и более убедительному виду, что одновременно усложняет саму генерацию волн. Поскольку нужную форму волн, которую хотят видеть океанологи или художники в 3D-пакетах, или программисты, разрабатывающие интерактивное приложение, задают множество параметров (высота и скорость волн, направление их движения, также направление и сила ветра и другие), то задача построения 3D-модели поверхности воды становится актуальной. А в задачах, где её еще необходимо и анимировать, важную роль играет не только реалистичность модели, но и скорость её построения. В этом разделе будут рассмотрены два алгоритма по созданию карты высот, используемой для построения морских волн. Вначале будет рассмотрен наиболее простой метод, когда волны образуются суммированием синусоид, потом будет рассмотрен метод создания модели на основе быстрого преобразования Фурье. 1.1 Волны Герстнера Волны Герстнера были впервые найдены около 200 лет назад в качестве приближенного решения гидродинамических уравнений. Свое первое применение в компьютерной графике они нашли в 1982 году Фурье и Ривзом. Эта физическая модель описывает поверхность с точки зрения движения её отдельных точек[4]. При внимательном рассмотрении, можно заметить, что эти 11 точки движутся по кругу, причем так, словно через них как раз проходит волна (см. рисунок 1). Рисунок 1 – Движение точки поверхности Если точку на невозмущенной поверхности пометить как 𝑋0 = (𝑥0 , 𝑦0 ), а её высоту как 𝑍0 = 0, то её волновое движение в момент времени 𝑡 можно описать в следующем виде ⃗ 𝑘 ⃗⃗⃗⃗⃗⃗⃗⃗ ⃗ · ⃗⃗⃗⃗ 𝑋(𝑡) = ⃗⃗⃗⃗ 𝑋0 − ( ) 𝐴𝑠𝑖𝑛(𝑘 𝑋0 − 𝜔𝑡); 𝑘 ⃗ · ⃗⃗⃗⃗ 𝑦 = 𝐴 𝑐𝑜𝑠(𝑘 𝑋0 − 𝜔𝑡), ⃗ – это горизонтальный вектор («волновой вектор»), значением где 𝑘 которого является направление движения волны; 𝐴 – это амплитуда волны; 𝑘 – величина, зависящая от длины волны как 𝑘 = 2𝜋 𝜆 ; 𝜔 – частота, относящаяся к волновому вектору. Она будет рассмотрена ниже. Как видно, волны Герстнера весьма ограничены, поскольку они представлены одной синусоидальной волной по горизонтали и вертикали. 12 Более сложный профиль может быть получен путем суммирования набора синусоид. Выбрав набор волновых векторов 𝑘𝑖 , амплитуд 𝐴𝑖 и начальных фаз 𝜑𝑖 , для 𝑖 = 1, … 𝑁 получаем выражения для результирующей волны 𝑁 ⃗⃗⃗ 𝑘𝑖 ⃗⃗⃗⃗⃗⃗⃗⃗ ⃗⃗⃗𝑖 ∙ ⃗⃗⃗⃗ ⃗⃗⃗⃗ 𝑋(𝑡) = 𝑋0 – ∑ ( ) 𝐴𝑖 𝑠𝑖𝑛(𝑘 𝑋0 − 𝜔𝑖 𝑡 + 𝜑𝑖 ) ; 𝑘𝑖 𝑖=1 𝑁 ⃗⃗⃗𝑖 ∙ 𝑋 ⃗⃗⃗⃗0 − 𝜔𝑖 𝑡 + 𝜑𝑖 ). 𝑦 = ∑ 𝐴𝑖 𝑐𝑜𝑠(𝑘 𝑖=1 На рисунке 2 представлен профиль волны, полученный путем сложения 4 синусоид. Рисунок 2 – Волна, полученная путем суммирования 4 простых синусоидальных волн 13 Анимация волн Герстнера определяется набором частот, для каждой компоненты из множества. Для волн на водной поверхности известна связь между этими частотами и величиной соответствующих волновых векторов ⃗⃗⃗ 𝑘𝑖 ⃗ ) = 𝑔𝑘 ⃗, 𝜔2 (𝑘 где g – это гравитационная постоянная, значение которой берут равным 9,8 м⁄сек2 . К сожалению, модель Герстнера не дает необходимого качества модели, поскольку в волнах наблюдается периодичность, а само поведение волн позволяют использовать эту модель лишь для водоемов со спокойной поверхностью (озера, реки). Для анимирования поверхности с более сильными волнами применяется статистическая модель. 1.2 Статистическая модель В физической океанографии разработан метод, позволяющий построить волновую картину исходя из атмосферных условий (скорости ветра, его направления и т.п.) не прибегая к решению уравнений движения, но лишь основываясь на экспериментальных наблюдениях и их статистической обработке и анализе. Этот метод так же, как и подход Герстнера, основан на предположении, что свободную поверхность с некоторыми допущениями можно представить в виде суммы гармоник различных длин и периодов [3]. В статистическом подходе для анализа их характеристик используется мощный математический аппарат дискретных преобразований Фурье, который позволяет на основании экспериментальных данных (в частности, измерений высоты волн через некоторый интервал времени), специальным образом сглаженных и обработанных, получить некоторое спектральное распределение, характерное для данных атмосферных условий. Эти распределения, в свою очередь, подвергаются анализу и аппроксимации, что позволяет построить некоторые аналитические приближения для спектра плотности мощности 14 волновой функции, которые получили название океанских спектров. В компьютерной графике ставится обратная задача – получить распределение высот океанской поверхности, исходя из выбранного спектра и атмосферных условий. Это возможно при помощи обратного преобразования Фурье, примененного к специальным образом обработанному полю случайных чисел. Как уже говорилось, в основе статистической модели синтеза поверхности океана лежит дискретное преобразование Фурье. Волна представляется как сумма большого числа гармоник (т.е. фактически осуществлялось разложение в ряд Фурье по пространственным переменным 𝑥 и 𝑦). Производя ДПФ над комплексной сеткой можно получить значения амплитуд волн в её узлах, то есть карту высот. Полученная с помощью преобразования Фурье карта высот периодична и её можно “натягивать” на сколь угодно большую поверхность. Соответственно, чем больше размер сетки для преобразования Фурье, тем больше полученная карта высот, тем более сложная поверхность и менее заметна периодичность волн, что сильно увеличивает реалистичность. Таким образом, карта высот статистической модели синтеза водной поверхности будет описываться следующей формулой ⃗ , 𝑡)𝑒 𝑖𝑘⃗𝑥 , ℎ(𝑥 , 𝑡) = ∑ ℎ̃(𝑘 ⃗ 𝑘 𝑛𝐿𝑥 𝑚𝐿𝑦 где 𝑥 = ( 𝑁 , 𝑀 ); ⃗ = (𝑘𝑥 , 𝑘𝑦 ), 𝑘𝑥 = 2𝜋𝑛 , 𝑘𝑦 = 2𝜋𝑚; 𝑘 𝐿𝑥 𝐿𝑦 (𝐿𝑥 , 𝐿𝑦 ) – размеры фрагмента водной поверхности; (𝑁, 𝑀) – размеры сетки для преобразования Фурье; − 𝑀 2 <𝑚< 𝑀 2 𝑁 𝑁 2 2 ,− < 𝑛 < . Чтобы синтезировать реалистичную водную поверхность необходимо подобрать способ выбора коэффициентов для преобразования Фурье. Также 15 необходимо решить вопрос о плавной анимации водной поверхности со временем. Коэффициенты ДПФ выбираются исходя из спектра Филлипса ⃗)=𝐴 𝑃ℎ (𝑘 где 𝐿 = 𝑉2 𝑔 exp (− 𝑘 1 ) (𝑘𝐿)2 ⃗ ⃗ |, |𝑘 ∙ 𝜔 4 – это максимально возможная волна, возникающая под действием непрерывного ветра со скоростью 𝑉 [5]; 𝜔 ⃗ – это сила и направление ветра; 𝐴 – нормирующая константа. Начальный спектр водной поверхности выражается в следующем виде ⃗) = ℎ̃0 (𝑘 1 √2 ⃗) , (𝜉𝑟 + 𝑖𝜉𝑖 )√𝑃ℎ (𝑘 где 𝜉𝑟 , 𝜉𝑖 – это случайные числа, полученные по закону нормального распределения. Общая диаграмма генерации начального спектра показана на рисунке 3. 16 Рисунок 3 – Диаграмма генерации начального спектра В дальнейшем синтезировать спектр Филлипса каждый кадр не нужно, достаточно ”анимировать” спектр с фиксированным по времени периодом. ⃗ , 𝑡) = ℎ̃0 (𝑘 ⃗ )𝑒 𝑖𝜔(𝑘)𝑡 + ℎ̃0∗ (−𝑘 ⃗ )𝑒 −𝑖𝜔(𝑘)𝑡 , ℎ̃(𝑘 где 𝜔(𝑘) = √𝑔𝑘; ℎ̃0∗ – число, сопряженное к ℎ̃0 . Можно заметить, что коэффициенты преобразования Фурье комплексные, хотя высота волны представляется вещественным числом. На самом деле, комплексная компонента не лишняя, она означает фазу волны. Формула ⃗ , 𝑡)𝑒 𝑖𝑘⃗𝑥 как раз и описывает сдвиг фазы волны. Таким образом, ℎ(𝑥 , 𝑡) = ∑𝑘⃗ ℎ̃(𝑘 сдвиг фаз всех гармоник дает плавную, и в то же время, непредсказуемую, анимацию поверхности. 17 2 Алгоритмы дискретного преобразования Фурье и способы их распараллеливания В данном разделе содержатся сведения об используемых алгебраических структурах: гиперкомплексной алгебре и прямой сумме комплексных алгебр, а также сведения об исследуемых алгоритмах. 2.1 Используемые алгебраические структуры 2.1.1 Алгебра гиперкомплексных чисел Приведем определение алгебры гиперкомплексных чисел [6]: 2𝑑 -мерная 𝑅-algebra 𝑩𝒅 с базисом 𝛼 𝜦 = {∏ 𝜀𝑖 𝑖 , 𝛼𝑖 ∈ {0; 1}; 𝐼 ∈ {1, … , 𝑑}} 𝑖∈𝐼 (1) называется коммутативно-ассоциативной гиперкомплексной алгеброй. Здесь 𝜀1 , 𝜀2 , … , 𝜀𝑑 – базис -мерного векторного пространства 𝑽, 𝜀𝑖0 = 1, 𝜀𝑖1 = 𝜀𝑖 , а их правило умножения задается соотношениями 𝜀𝑖 𝜀𝑗 = 𝜀𝑗 𝜀𝑖 , 𝜀𝑖2 = 𝛽𝑖 , 𝑖, 𝑗 ∈ 𝐼, 𝛽𝑖 = ±1 . (2) Произвольный элемент 𝑔 ∈ 𝑩𝑑 имеет вид 𝑔 = 𝜉0 𝐸0 + ⋯ + 𝜉2𝑑−1 𝐸2𝑑−1 = ∑ 𝜉𝑡 𝐸𝑡 , 𝑡∈𝑇 (3) где 𝛼 𝐸𝑡 = ∏ 𝜀𝑖 𝑖 , 𝑖∈𝐼 𝑡 = ∑ 𝛼𝑖 2𝑖−1 ∈ 𝑇 = {0,1, … , 2𝑑 − 1}. 𝑖∈𝐼 (4) 18 В этой алгебре сложение элементов выполняется покомпонентно, а умножение полностью определяется правилами умножения базисных элементов 𝐸𝑡 𝐸𝜏 = 𝛹(𝑡, 𝜏)𝐸𝑡⊕𝜏 , ∀𝑡, 𝜏 ∈ 𝑇 , (5) где ⊕ обозначает побитное сложение по модулю 2, ℎ (𝑡,𝜏) 𝛹(𝑡, 𝜏) = ∏ 𝛽𝑖 𝑖 , ℎ𝑖 (𝑡, 𝜏) = 𝛼𝑖 𝛼𝑖′ , 𝜏 = ∑ 𝛼𝑖 2𝑖−1 . 𝑖∈𝐼 𝑖∈𝐼 (6) Здесь мы рассматриваем только такую гиперкомплексную алгебру, которая изоморфна прямой сумме комплексных алгебр 𝑩𝑑 ≅ ⏟ 𝑪 ⊕ 𝑪 ⊕ …⊕ 𝑪 , 2𝑑−1 (7) что гарантирует минимальную сложность операции умножения в 𝑩𝑑 . В этом случае, по крайней мере, одному элементу 𝛼𝑖 соответствует 𝛽𝑖 = −1. В данной работе исследуется случай для 𝑑 = 2. Рассмотрим его. В этом случае 𝑩2 является четырехмерной коммутативно-ассоциативной алгеброй, произвольный элемент ℎ ∈ 𝑩2 которой имеет вид ℎ = 𝑎𝐸0 + 𝑏𝐸1 + 𝑐𝐸2 + 𝑑𝐸3 , 𝑎, 𝑏, 𝑐, 𝑑 ∈ 𝑹 , где 𝐸0 = 1, 𝐸1 = 𝜀1 , 𝐸2 = 𝜀2 , 𝐸3 = 𝜀1 𝜀2 . Представим ℎ в следующем виде ℎ = 𝑎 + 𝑏𝜀1 + 𝑐𝜀2 + 𝑑𝜀1 𝜀2 . Обычно, в математической литературе, для четырехмерной гиперкомплексной алгебры принято использовать другие обозначения [7]: 19 ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗, 𝑎, 𝑏, 𝑐, 𝑑 ∈ 𝑹 . (8) Соотношения для умножений базисных элементов {1, 𝑖, 𝑗, 𝑖𝑗} имеют вид: 𝑖 2 = 𝑗 2 = −1, 𝑖𝑗 = 𝑗𝑖, (𝑖𝑗)2 = 1 . (9) Соотношения (9) для умножения базисных элементов индуцируют правило умножения произвольных элементов алгебры 𝑩2 : ℎ𝑞 = (𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗)(𝑥 + 𝑦𝑖 + 𝑧𝑗 + 𝑤𝑖𝑗) = = (𝑎𝑥 − 𝑏𝑦 − 𝑐𝑧 + 𝑤𝑑) + +(𝑎𝑦 + 𝑏𝑥 − 𝑐𝑤 − 𝑑𝑧)𝑖 + +(𝑎𝑧 − 𝑏𝑤 + 𝑐𝑥 − 𝑑𝑦)𝑗 + +(𝑎𝑤 + 𝑏𝑧 + 𝑐𝑦 + 𝑑𝑥)𝑖𝑗 . (10) Отметим еще два полезных свойства алгебры 𝑩2 : поле комплексных чисел канонически вкладывается в 𝑩2 𝑎 + 𝑏𝑖 → 𝑎 + 𝑏𝑖 + 0 ∙ 𝑗 + 0 ∙ 𝑖𝑗 ; справедливы соотношения ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗 = (𝑎 + 𝑏𝑖) + (𝑐 + 𝑑𝑖)𝑗 = 𝑠0 + 𝑠1 𝑗 , ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗 = (𝑎 + 𝑏𝑖) + (𝑑 − 𝑐𝑖)𝑖𝑗 = 𝑧0 + 𝑧1 𝜀, (𝜀 = 𝑖𝑗; 𝜀 2 = 1) . (11) Непосредственное умножение элементов алгебры 𝑩2 по формуле (10) требует 16 вещественных умножений и 12 вещественных сложений. Как обычно, будем считать, что: при оценке вычислительной сложности один из сомножителей предполагается постоянным и, следовательно, все арифметические операции над его компонентами могут быть реализованы заранее; 20 умножения на степени числа 2 являются более простыми операциями, чем сложения и умножения, и не учитываются при анализе вычислительной сложности алгоритмов ДПФ. Пусть ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗 – элемент алгебры 𝑩2 общего вида, 𝑠 = 𝑥 + 𝑦𝑖 – комплексное число, являющееся константой в контексте рассматриваемых алгоритмов. В соответствии с представлением (11) вычисление произведения hs эквивалентно выполнению двух комплексных умножений. При использовании схемы "три умножения, три сложения" искомое произведение принимает вид ℎ𝑠 = ((𝑎 + 𝑏)𝑥 − 𝑏(𝑥 + 𝑦)) + +((𝑎 + 𝑏)𝑥 − 𝑎(𝑥 − 𝑦))𝑖 + +((𝑐 + 𝑑)𝑥 − 𝑑(𝑥 + 𝑦))𝑗 + +((𝑐 + 𝑑)𝑥 − 𝑐(𝑥 − 𝑦))𝑖𝑗 (12) и вычисляется посредством шести вещественных умножений и шести вещественных сложений. Одновременное умножение на два комплексных корня в этой алгебре лучше выполнять по очереди. Непосредственной проверкой легко убедиться также в справедливости следующего утверждения. Отображения 𝜀0 (ℎ) = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗 𝜀𝑖 (ℎ) = 𝑎 + 𝑏𝑖 − 𝑐𝑗 − 𝑑𝑖𝑗 𝜀𝑗 (ℎ) = 𝑎 − 𝑏𝑖 + 𝑐𝑗 − 𝑑𝑖𝑗 {𝜀𝑖𝑗 (ℎ) = 𝑎 − 𝑏𝑖 − 𝑐𝑗 + 𝑑𝑖𝑗 (13) сохраняют сумму и произведение элементов алгебры 𝑩2 , действуют тождественно на 𝑹 (то есть являются автоморфизмами 𝑩2 над 𝑹) [9]. 21 2.1.2 Прямая сумма комплексных алгебр Пусть произвольный элемент ℎ 2𝑑 -мерной алгебры 𝑩2 определен соотношением (3). Разобьем множество {𝐸𝑡 }𝑡∈𝑇 на две части: 𝑡 ∈ 𝑇′, если 𝐸𝑡 не включает в себя 𝜀1 , и 𝑡 ∈ 𝑇′′ в противном случае. Учитывая выбранный способ нумерации базисных элементов, в первую часть войдут базисные элементы 𝐸𝑡 с четными индексами 𝑡, а во вторую часть – с нечетными. Введем замену переменных: 𝑼𝟎 = 𝑨𝑬𝟎 , 𝑼𝟏 = 𝑨𝑬𝟏 , (14) где 𝑬𝟎 = {𝐸𝑡 }𝑡∈𝑇 ′ , 𝑬𝟏 = {𝐸𝑡 }𝑡∈𝑇 ′′ , 𝑼𝟎 = ( 𝑢0 𝑢1 ⋮ ), 𝑢2𝑑−1−1 1 1 1 ⋯ 1 1 1 1 ⋯ −1 𝑨 = 1 1 −1 ⋯ 1 . ⋮ ⋮ ⋮ ⋱ ⋮ (1 −1 1 ⋯ −1) 𝑢2𝑑−1 𝑢2𝑑−1+1 𝑼𝟏 = ( ⋮ ) , 𝑢2𝑑−1 Каждый столбец и каждая строка матрицы 𝑨 содержит ровно 2𝑑−2 отрицательных значений из 2𝑑−1 значений. Правила умножения новых базисных элементов имеют вид 𝑝𝑢𝑗 , при 𝑗 < 𝑝, 𝑢𝑗2 = {−𝑝𝑢 при 𝑗 ≥ 𝑝, 𝑗−2𝑑−1 , 𝑝𝑢𝑘 , если 𝑘 = 𝑗 + 𝑝, 𝑢𝑗 𝑢𝑗 = { 0, в противном случае, где 𝑝 = 2𝑑−1 . Отметим, что многие произведения равны нулю. Этот факт позволяет нам представить произведение двух произвольных элементов алгебры 𝑩𝑑 в следующем виде 𝑔 ∙ 𝑠 = ∑ 𝜉𝑡 𝐸𝑡 ∙ ∑ 𝜍𝜏 𝐸𝜏 = ∑ 𝑎𝑡 𝑢𝑡 ∙ ∑ 𝑏𝜏 𝑢𝜏 = 𝑡∈𝑇 𝜏∈𝑇 𝑡∈𝑇 22 𝜏∈𝑇 = ∑(𝑎𝑡 𝑢𝑡 + 𝑎𝑡+𝑝 𝑢𝑡+𝑝 )(𝑏𝑡 𝑢𝑡 + 𝑏𝑡+𝑝 𝑢𝑡+𝑝 ) . 𝑡∈𝑇 ′ Таким образом, произведение двух произвольных элементов алгебры 𝑩𝑑 сводится к 𝑝 независимым произведениям комплексных чисел, каждое из которых требует трех вещественных умножений и трех вещественных сложений. В данной работе исследуется случай для 𝑑 = 2. Рассмотрим его. Известно, что коммутативно-ассоциативная гиперкомплексная алгебра 𝑩𝑑 изоморфна прямой сумме комплексных алгебр 𝐵 =𝐶⊕𝐶. Для перехода от представления гиперкомплексного числа ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗 (15) к его представлению в прямой сумме комплексных алгебр, выполним замену переменных [10]: 𝑢0 = 1 + 𝑖𝑗, 𝑢1 = 1 − 𝑖𝑗, 𝑢2 = 𝑖 − 𝑗, 𝑢3 = 𝑖 + 𝑗 . (16) В этом случае произвольный элемент алгебры примет 1 ℎ = ((𝑎 + 𝑑)𝑢0 + (𝑎 − 𝑑)𝑢1 + (𝑏 − 𝑐)𝑢2 + (𝑏 + 𝑐)𝑢3 ). 2 Правила умножения новых базисных элементов показаны в таблице 1. 23 вид Таблица 1 – Правила умножения новых базисных элементов для d=2 Базисные 𝑢0 𝑢1 𝑢2 𝑢3 𝑢0 2𝑢0 0 2𝑢2 0 𝑢1 0 2𝑢1 0 2𝑢3 𝑢2 2𝑢2 0 −2𝑢0 0 𝑢3 0 2𝑢3 0 −2𝑢1 элементы В таком представлении произведение двух произвольных элементов алгебры (𝑥𝑢0 + 𝑦𝑢1 + 𝑧𝑢2 + 𝑣𝑢3 )(𝑎𝑢0 + 𝑏𝑢1 + 𝑐𝑢2 + 𝑑𝑢3 ) разбивается на два независимых произведения, подобных произведениям комплексных чисел (𝑥𝑢0 + 𝑧𝑢2 )(𝑎𝑢0 + 𝑐𝑢2 ) = 2((𝑥𝑎 − 𝑧𝑐)𝑢0 + (𝑥𝑐 + 𝑧𝑎)𝑢2 ) , (𝑦𝑢1 + 𝑣𝑢3 )(𝑏𝑢1 + 𝑑𝑢3 ) = 2((𝑦𝑏 − 𝑣𝑑)𝑢1 + (𝑦𝑑 + 𝑣𝑏)𝑢3 ) , (17) и требует 6 вещественных умножений и 6 вещественных сложений. Все свойства гиперкомплексной алгебры в таком представлении сохраняются. Рассмотрим переход от представления числа в прямой сумме комплексных алгебр в представление в гиперкомплексной алгебре. Пусть 𝑧 = 𝛼𝑢0 + 𝛽𝑢1 + 𝛾𝑢2 + 𝛿𝑢3 – число в прямой сумме комплексных алгебр. Воспользуемся заменой переменных (16). Получим 𝑧 = 𝛼𝑢0 + 𝛽𝑢1 + 𝛾𝑢2 + 𝛿𝑢3 = 𝛼(1 + 𝑖𝑗) + 𝛽(1 − 𝑖𝑗) + 𝛾(𝑖 − 𝑗) + 𝛿(𝑖 + 𝑗) = = (𝛼 + 𝛽) + (𝛾 + 𝛿)𝑖 + (𝛿 − 𝛾)𝑗 + (𝛼 − 𝛽)𝑖𝑗 . Для прямой суммы комплексных алгебр выпишем соответствующие автоморфизмы, используя (13) и (16) 24 𝜀𝑢0 (𝑧) = 𝛼𝑢0 + 𝛽𝑢1 + 𝛾𝑢2 + 𝛿𝑢3 ; 𝜀𝑢1 (𝑧) = 𝛽𝑢0 + 𝛼𝑢1 + 𝛿𝑢2 + 𝛾𝑢3 ; 𝜀𝑢2 (𝑧) = 𝛽𝑢0 + 𝛼𝑢1 − 𝛿𝑢2 − 𝛾𝑢3 ; {𝜀𝑢3 (𝑧) = 𝛼𝑢0 + 𝛽𝑢1 − 𝛾𝑢2 − 𝛿𝑢3 . (18) 2.2 Одномерное ДПФ Рассмотрим выражение для дискретного преобразования Фурье: 𝑁−1 𝑋(𝑘) = ∑ 𝑥(𝑛) ∙ exp (−𝑖 𝑛=0 2𝜋 𝑛𝑘) , 𝑁 𝑘 = 0. . 𝑁 − 1 . (19) ДПФ 𝑁 отсчетам сигнала 𝑥(𝑛), 𝑛 = 0. . 𝑁 − 1 (в общем случае комплексным) ставит в соответствие 𝑁 комплексных отсчетов спектра 𝑋(𝑘), 𝑘 = 0. . 𝑁 − 1, причем для вычисления одного спектрального отсчета требуется 𝑁 операций комплексного умножения и сложения [8]. Таким образом, вычислительная сложность прямого вычисления ДПФ составляет 𝑁2 комплексных умножений и сложений. При этом можно заметить, что если одно ДПФ на 𝑁 точек (отсчетов) заменить вычислением двух ДПФ по 𝑁⁄2 точек, то это приведет к уменьшению количества операций. При этом каждое из 𝑁⁄2-точечных ДПФ также можно вычислить путем замены 𝑁⁄2-точечного ДПФ на два 𝑁⁄4-точечных. Таким образом, можно продолжать разбиение исходной последовательности до тех пор, пока возможно деление последовательности на две. Очевидно, что если 𝑁 = 2𝐿 , 𝐿 – положительное целое, мы можем разделить последовательность пополам 𝐿 раз. Для 𝑁 = 8 (𝐿 = 3) такое разбиение представлено на рисунке 4. 25 ДПФ N=2 Разб. 𝑥(6) 𝑥(7) Объединение 𝑋(1) 𝑋(2) Объединение Объед. Объед. 𝑋(0) Объединение Разбиение 𝑥(5) Объед. ДПФ N=2 Разб. Разбиение 𝑥(4) ДПФ N=2 Разб. 𝑥(2) Объед. Разбиение 𝑥(1) 𝑥(3) ДПФ N=2 Разб. 𝑥(0) 𝑋(3) 𝑋(4) 𝑋(5) 𝑋(6) 𝑋(7) Рисунок 4 –Разбиение и объединение последовательностей при N = 8 Каждое разбиение делит последовательность на две подпоследовательности половинной длины, а каждое объединение «собирает» из двух последовательностей одну удвоенную. Разбиение исходной последовательности прореживанием по времени. Для начала комплексную экспоненту в выражении (19) обозначим как exp (−𝑖 2𝜋 𝑛𝑘 𝑛𝑘) = 𝜔𝑁 . 𝑁 (20) Тогда выражение (19) принимает вид 𝑁−1 𝑛𝑘 𝑋(𝑘) = ∑ 𝑥(𝑛)𝜔𝑁 , 𝑘 = 0. . 𝑁 − 1 . 𝑛=0 (21) Прореживание по времени заключается в разбиении исходной последовательности отсчетов 𝑥(𝑛), 𝑛 = 0. . 𝑁 − 1 на две последовательности длины 𝑁/2 𝑥0 (𝑛) и 𝑥1 (𝑛), 𝑛 = 0. . 𝑁⁄2 − 1, таких что 𝑥0 (𝑛) = 𝑥(2𝑛), а 𝑥1 (𝑛) = 𝑥(2𝑛 + 1), 𝑛 = 0. . 𝑁⁄2 − 1. 26 Рассмотрим ДПФ сигнала прореженного по времени 𝑁⁄2−1 𝑁⁄2−1 (2𝑛+1)𝑘 2𝑛𝑘 𝑋(𝑘) = ∑ 𝑥(2𝑛)𝜔𝑁 + ∑ 𝑥(2𝑛 + 1)𝜔𝑁 𝑛=0 = 𝑛=0 𝑁⁄2−1 𝑁⁄2−1 2𝑛𝑘 2𝑛𝑘 = ∑ 𝑥(2𝑛)𝜔𝑁 + 𝑊𝑁𝑘 ∑ 𝑥(2𝑛 + 1)𝜔𝑁 , 𝑛=0 𝑘 = 0. . 𝑁 − 1 . 𝑛=0 (22) Если рассмотреть только первую половину спектра 𝑋(𝑘), 𝑘 = 0. . 𝑁⁄2 − 1, а также учесть что 2𝑛𝑘 𝜔𝑁 = exp (−𝑖 2𝜋 𝑛𝑘 2𝑛𝑘) = 𝜔𝑁 ⁄2 , 𝑁 (23) тогда (22) можно записать: 𝑁⁄2−1 𝑁⁄2−1 𝑛𝑘 𝑛𝑘 𝑘 𝑋(𝑘) = ∑ 𝑥(2𝑛)𝜔𝑁 ⁄2 + 𝜔𝑁 ∑ 𝑥(2𝑛 + 1)𝜔𝑁⁄2 = где 𝑛=0 𝑛=0 𝑘 = 𝑋0 (𝑘) + 𝜔𝑁 𝑋1 (𝑘), 𝑘 = 0. . 𝑁⁄2 − 1, 𝑋0 (𝑘) и 𝑋1 (𝑘), – 𝑘 = 0. . 𝑁⁄2 − 1 𝑁⁄2-точечные ДПФ последовательностей 𝑥0 (𝑛) и 𝑥1 (𝑛), 𝑛 = 0. . 𝑁⁄2 − 1: 𝑁⁄2−1 𝑛𝑘 𝑋0 (𝑘) = ∑ 𝑥0 (𝑛)𝜔𝑁 ⁄2 , 𝑘 = 0. . 𝑁⁄2 − 1 , 𝑛=0 𝑁⁄2−1 𝑛𝑘 𝑋1 (𝑘) = ∑ 𝑥1 (𝑛)𝜔𝑁 ⁄2 , 𝑘 = 0. . 𝑁⁄2 − 1 . 𝑛=0 Прореживание по времени можно считать алгоритмом разбиения последовательности на две подпоследовательности половинной длины. Первая половина объединенного спектра есть сумма спектра «четной» последовательности и спектра «нечетной» последовательности, умноженного 𝑘 на коэффициенты 𝜔𝑁 , которые носят названия поворотных коэффициентов. 27 Процедура объединения. Теперь рассмотрим вторую половину спектра 𝑋(𝑘 + 𝑁⁄2), 𝑘 = 0. . 𝑁⁄2 − 1: 𝑁⁄2−1 𝑛(𝑘+𝑁⁄2) 𝑋(𝑘 + 𝑁⁄2) = ∑ 𝑥(2𝑛)𝜔𝑁/2 + 𝑛=0 𝑁⁄2−1 (𝑘+𝑁⁄2) +𝜔𝑁 𝑛(𝑘+𝑁⁄2) ∑ 𝑥(2𝑛 + 1)𝜔𝑁⁄2 𝑘 = 0. . 𝑁⁄2 − 1. , 𝑛=0 (24) Рассмотрим подробнее множитель 𝑛(𝑘+𝑁/2) 𝜔𝑁/2 𝑛∙𝑁/2 𝑛𝑘 = 𝜔𝑁/2 𝜔𝑁/2 . (25) Учтем, что 𝑛∙𝑁/2 𝜔𝑁/2 = exp (−𝑖 2𝜋 𝑁 𝑛 ∙ ) = exp(−𝑖2𝜋𝑛) = 1 , 𝑁 2 2 (26) тогда выражение (26) справедливо для любого целого 𝑛. В таком случае выражение (25) с учетом (26) можно записать как 𝑛(𝑘+𝑁/2) 𝜔𝑁/2 𝑛𝑘 = 𝜔𝑁/2 . (27) Рассмотрим теперь поворотный коэффициент в (24) (𝑘+𝑁⁄2) 𝜔𝑁 ⁄ 𝑘 𝑘 = 𝜔𝑁𝑁 2 𝜔𝑁 = −𝜔𝑁 . (28) Тогда выражение (24) с учетом (26) и (26) принимает вид 𝑁⁄2−1 𝑁⁄2−1 𝑛𝑘 𝑛𝑘 𝑘 𝑋(𝑘 + 𝑁⁄2) = ∑ 𝑥(2𝑛)𝜔𝑁 ⁄2 − 𝜔𝑁 ∑ 𝑥(2𝑛 + 1)𝜔𝑁⁄2 = 𝑛=0 𝑛=0 28 𝑘 = 𝑋0 (𝑘)−𝜔𝑁 𝑋1 (𝑘), 𝑘 = 0. . 𝑁⁄2 − 1. Таким образом, окончательно можно записать 𝑘 𝑋(𝑘) = 𝑋0 (𝑘)−𝜔𝑁 𝑋1 (𝑘), 𝑘 = 0. . 𝑁/2 − 1; 𝑁 𝑘 𝑋 (𝑘 + ) = 𝑋0 (𝑘)−𝜔𝑁 𝑋1 (𝑘), 2 𝑘 = 0. . 𝑁/2 − 1. (29) Выражение (29) представляет собой алгоритм объединения при прореживании по времени. Данную процедуру объединения можно представить в виде графа (рисунок 5), который называется «Бабочка». 𝑋0 (𝑘) 𝑘 𝜔𝑁 ∙ 𝑋1 (𝑘) 𝑘 𝑋(𝑘) = 𝑋0 (𝑘) + 𝜔𝑁 ∙ 𝑋1 (𝑘) 𝑘 𝑋(𝑘 + 𝑁/2) = 𝑋0 (𝑘) − 𝜔𝑁 ∙ 𝑋1 (𝑘) Рисунок 5 – Процедура объединения на основе графа « Бабочка » Алгоритм с прореживанием по времени на каждом уровне требует 𝑁 комплексных умножений и сложений. При 𝑁 = 2𝐿 количество уровней разложения — объединения равно 𝐿, таким образом, общее количество операций умножения и сложения равно 𝐿 ∙ 𝑁. Рассмотрим во сколько раз алгоритм БПФ с прореживанием по времени эффективнее ДПФ. Для этого рассмотрим коэффициент ускорения 𝐾 отношение количества комплексных умножение и использовании ДПФ и БПФ, при этом учтем что 𝐿 = 𝑙𝑜𝑔2 (𝑁): 𝑁2 𝑁 𝐾= = . 𝑁 ∙ 𝐿 𝑙𝑜𝑔2 (𝑁) 29 сложений при 2.3 Двумерное ДПФ 2.3.1 Построчно-столбцовый алгоритм ДПФ Одномерное преобразование Фурье определяется формулой 𝑁−1 𝑋(𝑘) = ∑ 𝑥(𝑛)𝜔𝑛𝑘 , 𝑘 = 0. . 𝑁 − 1 , 𝑛=0 (30) 2𝜋 где 𝜔 = 𝑒 −𝑖 𝑁 . В таком случае двумерное преобразование Фурье, аналогично одномерному (30), будет определяться по формуле: 𝑁−1 𝑁−1 𝑋(𝑚1 , 𝑚2 ) = ∑ ∑ 𝑥(𝑛1 , 𝑛2 )𝜔 𝑛1𝑚1+𝑛2𝑚2 , 0 ≤ 𝑚1 , 𝑚2 ≤ 𝑁 − 1 . 𝑛1 =0 𝑛2 =0 (31) Если в распоряжении есть алгоритм одномерного ДПФ, то двумерное преобразование Фурье можно свести к последовательному одномерному преобразованию вначале всех строк исходной матрицы, а затем и всех столбцов[9]. Действительно, 𝜔 𝑛1𝑚1+𝑛2𝑚2 = 𝜔 𝑛1𝑚1 ∙ 𝜔 𝑛2𝑚2 , (32) откуда получаем с учетом (31) и (32) 𝑁−1 𝑁−1 𝑋(𝑚1 , 𝑚2 ) = ∑ [ ∑ 𝑥(𝑛1 , 𝑛2 )𝜔 𝑛2𝑚2 ] 𝜔 𝑛1𝑚1 . 𝑛1 =0 𝑛2 =0 Введя обозначение 30 𝑁−1 𝑋 ′(𝑚1,𝑚2) = ∑ 𝑥(𝑛1 , 𝑛2 )𝜔 𝑛2𝑚2 , 𝑛2 =0 (33) получим 𝑁−1 𝑋(𝑚1 , 𝑚2 ) = ∑ 𝑋′(𝑚1 , 𝑚2 )𝜔 𝑛1𝑚1 . 𝑛1 =0 (34) В свою очередь каждое одномерное ДПФ выполняется с применением быстрого алгоритма. Сложность вычислений на первом этапе при использовании построчностолбцового алгоритма составит 𝑁 раз по 𝑁𝑙𝑜𝑔2 (𝑁) [см. раздел 2.2] базовых операций, под которыми понимаются операции вычисления выражения под знаком суммы в формулах (33) и (34), и столько же на втором, откуда: 𝑄БПФ = 2𝑁 2 𝑙𝑜𝑔2 (𝑁). В то же время непосредственные вычисления двумерного ДПФ по формуле (31) требуют вычислительных затрат: 𝑄ДПФ = 𝑁 4 . Таким образом, построчно-столбцовый метод позволят существенно снизить вычислительную сложность алгоритма двумерного ДПФ с 𝑁 4 до 2𝑁 2 𝑙𝑜𝑔2 (𝑁). Вычисление одномерного ДПФ по каждой из координат выполняется на основе процедуры быстрого преобразования Фурье. При этом преобразование по следующей координате может выполняться только после того, как сформирована вся матрица промежуточных результатов. 2.3.2 Алгоритм двумерного ДПФ по основанию 2 Используя (31), представим двумерное ДПФ в виде четырех сумм, разделяя входную последовательность по четным и нечетным значениям каждого индекса 𝑛1 , 𝑛2 31 𝑁−1 𝑋(𝑚1 , 𝑚2 ) = ∑ 𝜔 𝑚1𝑛1 𝑥(𝑛1 , 𝑛2 )𝜔 𝑚2𝑛2 = 𝑛1 ,𝑛2 =0 𝑁 −1 2 1 = ∑ 𝜔 𝑎𝑚1 ∑ (𝜔2 )𝑚1𝑛1 𝑥𝑎𝑏 (𝑛1 , 𝑛2 )(𝜔2 )𝑚2𝑛2 𝜔 𝑏𝑚2 = 𝑎,𝑏=0 𝑛1 ,𝑛2 =0 1 = ∑ 𝜔 𝑎𝑚1 𝑋𝑎𝑏 (𝑚1 , 𝑚2 )𝜔 𝑏𝑚2 , 𝑎,𝑏=0 (35) где 𝑥𝑎𝑏 (𝑛1 , 𝑛2 ) = 𝑥(2𝑛1 + 𝑎, 2𝑛2 + 𝑏) , 𝑁 −1 2 𝑋𝑎𝑏 (𝑚1 , 𝑚2 ) = ∑ (𝜔2 )𝑚1𝑛1 𝑥𝑎𝑏 (𝑛1 , 𝑛2 )(𝜔2 )𝑚2𝑛2 , 𝑛1 ,𝑛2 =0 0 ≤ 𝑚1 , 𝑚2 ≤ 𝑁 −1. 2 Вычисление спектра для остальных значений пар (𝑚1 , 𝑚2 ) производится без дополнительных умножений и может быть записано в матричной форме 𝑋(𝑚1 , 𝑚2 ) 𝑁 𝑋 (𝑚 + ,𝑚 ) 1 1 21 2 1 −1 𝑁 =[ 1 1 𝑋 (𝑚1 , 𝑚2 + ) 2 1 −1 𝑁 𝑁 [𝑋 (𝑚1 + 2 , 𝑚2 + 2 )] 𝑋00 (𝑚1 , 𝑚2 ) 1 1 𝑚1 𝜔 𝑋10 (𝑚1 , 𝑚2 ) 1 −1 ]× 𝑋01 (𝑚1 , 𝑚2 )𝜔 𝑚2 −1 −1 −1 1 [𝜔 𝑚1 𝑋11 (𝑚1 , 𝑚2 )𝜔 𝑚2 ] (36) или в следующем виде 1 𝑁 𝑁 𝑋 (𝑚1 + 𝑟 , 𝑚2 + 𝑠 ) = ∑ (−1)𝑎𝑟 𝜔 𝑎𝑚1 𝑋𝑎𝑏 (𝑚1 , 𝑚2 )𝜔 𝑏𝑚2 (−1)𝑏𝑠 , 2 2 𝑎,𝑏=0 (37) где 𝑟, 𝑠 = 0,1. Определение двумерного ДПФ в алгебре комплексных чисел и в четырехмерных алгебрах. Под дискретным двумерным преобразованием 32 Фурье со специальным представлением данных будем понимать преобразование вида 𝑁−1 𝑁−1 𝑛 𝑚 𝑛 𝑚 𝑋̃ (𝑚1 , 𝑚2 ) = ∑ ∑ 𝑥(𝑛1 , 𝑛2 )𝜔1 1 1 𝜔2 2 2 . 𝑛1 =0 𝑛2 =0 (38) Основная идея такого преобразования заключается в погружении входного сигнала и корней 𝜔𝑘 в четырехмерную алгебру 𝑨 (гиперкомплексная алгебра 2𝜋 или прямая сумма комплексных алгебр). При этом корни 𝜔1 = 𝑒 −𝑖 𝑁 , 2𝜋 𝜔2 = 𝑒 −𝑗 𝑁 лежат в разных экземплярах поля комплексных чисел, вложенных в 𝑩𝟐 и у них разные мнимые единицы – 𝑖 и 𝑗. Представим корни в следующем виде 𝑛 𝑚1 𝜔1 1 𝑛 𝑚2 𝜔2 2 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) + 𝑖𝑠𝑖𝑛 ( ), 𝑁 𝑁 2𝜋𝑚2 𝑛2 2𝜋𝑚2 𝑛2 = cos ( ) + 𝑗𝑠𝑖𝑛 ( ). 𝑁 𝑁 = cos ( (39) Тогда, используя (10) и (39), запишем произведение произвольного элемента гиперкомплексной алгебры на один из корней 𝑚 1 𝑛1 ℎ𝜔1 = ((𝑎 + 𝑏) cos ( + ((𝑎 + 𝑏) cos ( 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) − 𝑏 (cos ( ) + sin ( ))) + 𝑁 𝑁 𝑁 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) − 𝑎 (cos ( ) − sin ( ))) 𝑖 + 𝑁 𝑁 𝑁 + ((𝑐 + 𝑑) cos ( 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) − 𝑑 (cos ( ) + sin ( ))) 𝑗 + 𝑁 𝑁 𝑁 + ((𝑐 + 𝑑) cos ( 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) − 𝑐 (cos ( ) − sin ( ))) 𝑖𝑗 . 𝑁 𝑁 𝑁 𝑚 2 𝑛2 Умножение на комплексный корень 𝜔2 33 записывается аналогично. Используя (17) запишем представление корней 𝜔1 и 𝜔2 в прямой сумме комплексных алгебр 1 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 = (cos ( ) 𝑢0 + cos ( ) 𝑢1 + 2 𝑁 𝑁 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 + sin ( ) 𝑢2 + + sin ( ) 𝑢3 ) , 𝑁 𝑁 1 2𝜋𝑚2 𝑛2 2𝜋𝑚2 𝑛2 𝑚 𝑛 𝜔2 2 2 = (cos ( ) 𝑢0 + cos ( ) 𝑢1 + 2 𝑁 𝑁 2𝜋𝑚2 𝑛2 2𝜋𝑚2 𝑛2 + sin ( ) 𝑢2 + sin ( ) 𝑢3 ) . 𝑁 𝑁 𝑚 1 𝑛1 𝜔1 (40) Тогда, используя (17) и (40), запишем произведение произвольного элемента прямой суммы комплексных алгебр 𝑧 = 𝑥𝑢0 + 𝑦𝑢1 + 𝑧𝑢2 + 𝑣𝑢3 на один из корней 𝑚 1 𝑛1 𝑧𝜔1 = (𝑥𝑐𝑜𝑠 ( + (𝑦𝑐𝑜𝑠 ( + (𝑥𝑠𝑖𝑛 ( 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) − 𝑧𝑠𝑖𝑛 ( )) 𝑢0 + 𝑁 𝑁 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) − 𝑣𝑠𝑖𝑛 ( )) 𝑢1 + 𝑁 𝑁 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) − 𝑧𝑐𝑜𝑠 ( )) 𝑢2 + 𝑁 𝑁 + (𝑦𝑠𝑖𝑛 ( 2𝜋𝑚1 𝑛1 2𝜋𝑚1 𝑛1 ) − 𝑣𝑐𝑜𝑠 ( )) 𝑢3 . 𝑁 𝑁 𝑚 2 𝑛2 Умножение на комплексный корень 𝜔2 Поскольку элемент алгебры 𝑩𝟐 записывается аналогично. определяется набором четырех вещественных чисел (𝑎, 𝑏, 𝑐, 𝑑), то комплексный спектр может быть получен из спектра в алгебре 𝑩𝟐 следующим образом 𝑋̃(𝑚1 , 𝑚2 ) = 𝑋(𝑚1 , 𝑚2 )𝑳𝑰 , (41) 34 где 𝑋(𝑚1 , 𝑚2 ) = (𝜒0 (𝑚1 , 𝑚2 ), 𝜒1 (𝑚1 , 𝑚2 ), 𝜒2 (𝑚1 , 𝑚2 ), 𝜒3 (𝑚1 , 𝑚2 )) – вектор компонент спектра, 1 0 𝑳=( 0 −1 0 1 ), 1 0 1 𝑰=( ). 𝑖 (42) Комплексный спектр (31) также может быть получен из спектра (38) в прямой сумме комплексных алгебр. Для этого также будем использовать (41), но матрица 𝑳 будет иметь вид, отличный от (42). Используя представление (42) для алгебры 𝑩𝟐 ,, преобразуем (41) к виду 0 2 𝑳=( 0 0 0 0 ), 0 2 1 𝑰=( ). 𝑖 (43) Итак, с помощью (41) и полученного представления (43) матриц 𝑳 и 𝑰, можно получить комплексный спектр (31) из спектра (38) в прямой сумме комплексных алгебр. Таким образом, мультипликативная сложность вычисления 𝑋̃(𝑚1 , 𝑚2 ) совпадает со сложностью вычисления спектра в алгебре 𝑨, т.к. умножения на матрицы 𝑳, 𝑰 не требуют выполнения нетривиальных операций вещественного умножения. 2.3.3 Учет симметрий спектра вещественного сигнала Основное свойство спектра сигнала в рассматриваемых алгебрах – это его симметрия. Так, спектр вещественного сигнала удовлетворяет следующим свойствам симметрии: 35 𝑋(𝑁 − 𝑚1 , 𝑚2 ) = 𝜀𝑗 (𝑋(𝑚1 , 𝑚2 )) , 𝑋(𝑚1 , 𝑁 − 𝑚2 ) = 𝜀𝑖 (𝑋(𝑚1 , 𝑚2 )) , 𝑋(𝑁 − 𝑚1 , 𝑁 − 𝑚2 ) = 𝜀𝑘 (𝑋(𝑚1 , 𝑚2 )) , (44) где 𝜀𝑗 , 𝜀𝑖 , 𝜀𝑘 – это по-прежнему автоморфизмы соответствующей алгебры (13) или (18). Такое свойство позволяет на каждом шаге алгоритма (см. п.2.7) 𝑝𝑘 𝑝𝑘 производить вычисления по формуле (37) в области размером ( ⁄2 , ⁄2) (см. рисунок 6), где 𝑝 - основание алгоритма, 𝑘 - номер шага декомпозиции. Недостающие отсчеты заполняются на основании свойств (44) в соответствии со схемой, показанной на рисунке 7. Рисунок 6 – Области, которые достаточно просчитать по формуле (37) 36 Рисунок 7 – Заполнение недостающих областей на основании свойств симметрии 2.4 Способы распараллеливания 2.4.1 По компонентам в построчно-столбцовом алгоритме Построчно-столбцовый алгоритм подразумевает под собой поочередную обработку вначале всех строк, а затем и всех столбцов. Таким образом, логично разделить между процессами строки и столбцы, которые они будут вычислять. Таким образом, параллельный алгоритм будет выглядеть следующим образом: Шаг 1. Изначальная перестановка отсчетов сигнала. Шаг 2. Распределение строк матрицы отсчетов между процессорами. Вычисление последовательных одномерных ДПФ с помощью быстрых алгоритмов. Шаг 3. Синхронизация процессоров. 37 Шаг 4. Распределение столбцов матрицы отсчетов между процессорами. Вычисление последовательных одномерных ДПФ с помощью быстрых алгоритмов. Шаг 5. Синхронизация процессоров. 2.4.2 Распараллеливание по структуре декомпозиции для двумерного случая Рассмотрим возможность параллельной реализации вычисления двумерного ДПФ (31) для схемы декомпозиции ДПФ «по основанию 2». Напомним, что основное соотношение по аналогии с БПФ Кули-Тьюки имеет вид 1 𝑎𝑚1 𝑋(𝑚1 , 𝑚2 ) = ∑ 𝜔1 𝑏𝑚2 𝑋𝑎𝑏 (𝑚1 , 𝑚2 )𝜔2 , 0 ≤ 𝑚1 , 𝑚2 ≤ 𝑎,𝑏=0 𝑁 −1, 2 (45) где 𝑁 −1 2 𝑋𝑎𝑏 (𝑚1 , 𝑚2 ) = ∑ (𝜔12 )𝑚1𝑛1 𝑥(2𝑛1 + 𝑎, 2𝑛2 + 𝑏)(𝜔22 )𝑚2𝑛2 . 𝑛1 ,𝑛2 =0 Ключевой операцией в таком алгоритме является реконструкция полного спектра 𝑋(𝑚1 , 𝑚2 ) по известным (найденным) значениям частичных спектров 𝑋𝑎𝑏 (𝑚1 , 𝑚2 ) размером 𝑁 2 𝑁 × . Предположим, что каждый частичный спектр 2 будет рассчитан на отдельном процессоре, причем время работы процессоров будет примерно одинаковым (так как одинаковы размеры массивов вычисляемых спектров и алгоритм их вычисления). Тогда параллельный алгоритм состоит из следующих шагов: Шаг 1. Распределение данных по процессорам. Шаг 2. Расчет частичных спектров на каждом процессоре. 38 Шаг 3. Умножение элементов частичных спектров на степени 𝜔1 , 𝜔2 . Шаг 4. Передача результатов на один процессор. Шаг 5. Объединение спектров по формуле (45). Ясно, что таким способом процесс может быть распараллелен на любое число процессоров, кратное четырем, за счет нескольких шагов декомпозиции типа (45). Предполагаемое время вычисления спектра обратно пропорционально числу процессоров, так как основные вычислительные затраты приходятся на расчет набора ДПФ уменьшенного размера. В случае размерности данных 𝑑 > 2 может быть применена аналогичная схема с распараллеливанием на каждом шаге на 2𝑑 процессов. 2.4.3 Распараллеливание за счет структуры алгебры для двумерного случая Такой способ может быть применен только при использовании представления данных в ассоциативно-коммутативной гиперкомплексной алгебре. Основан он на переходе к представлению данных в прямой сумме комплексных алгебр 𝑩𝟐 ≅ 𝑪 ⊕ 𝑪 . Рассмотрим принципы формирования такого параллельного алгоритма. Пусть выполнена замена переменных, описанная в подразделе 2.1.2 для случая 𝑑 = 2. Повторим ее здесь для удобства изложения. Итак, представим произвольный элемент гиперкомплексной алгебры 𝑩𝟐 в форме ℎ = 𝑎 + 𝑏𝑖 + 𝑐𝑗 + 𝑑𝑖𝑗 и выполним замену переменных: 𝑢0 = 1 + 𝑖𝑗, 𝑢1 = 1 − 𝑖𝑗, 𝑢2 = 𝑖 − 𝑗, 𝑢3 = 𝑖 + 𝑗 . В новом представлении гиперкомплексное число ℎ запишется в виде 39 1 ℎ = ((𝑎 + 𝑑)𝑢0 + (𝑎 − 𝑑)𝑢1 + (𝑏 − 𝑐)𝑢2 + (𝑏 + 𝑐)𝑢3 ) . 2 Очевидно, что переход к новому представлению потребует четырех вещественных сложений. Однако для вещественных (входной сигнал) и комплексных (корни 𝜔𝑘 ) чисел этот переход не потребует выполнения нетривиальных арифметических операций. Обратный переход к исходному представлению также требует четырех вещественных сложений на отсчет гиперкомплексного спектра. Правила умножения новых базисных элементов приведены в таблице 1. Часть произведений новых базисных элементов 𝑢𝑘 в ней равна нулю. На этом факте и строится параллельный алгоритм. Конкретно будут равны нулю произведения элементов 𝑢0 и 𝑢2 с элементами 𝑢1 и 𝑢3 . Это означает, что вычисление произведения двух произвольных гиперкомплексных чисел состоит в таком представлении из двух совершенно независимых частей. Вместо произведения (𝑥𝑢0 + 𝑦𝑢1 + 𝑧𝑢2 + 𝑣𝑢3 )(𝛼𝑢0 + 𝛽𝑢1 + 𝛾𝑢2 + 𝛿𝑢3 ) теперь достаточно независимо вычислить два произведения: (𝑥𝑢0 + 𝑧𝑢2 )(𝛼𝑢0 + 𝛾𝑢2 ) = 2((𝑥𝛼 − 𝑧𝛾)𝑢0 + (𝑥𝛾 + 𝑧𝛼)𝑢2 ), (𝑦𝑢1 + 𝑣𝑢3 )(𝛽𝑢1 + 𝛿𝑢3 ) = 2((𝑦𝛽 − 𝑣𝛿)𝑢1 + (𝑦𝛿 + 𝑏𝛽)𝑢3 ). В процессе вычисления ДПФ необходимо выполнять две ключевых операции. Это умножение гиперкомплексного числа на степени корней 𝜔1 , 𝜔2 и сложение гиперкомплексных чисел. Таким образом, наиболее трудоемкая операция быстрого алгоритма двумерного ДПФ – вычисление произведения гиперкомплексных чисел – может быть распараллелена на две независимые ветви, не требующие обмена данными. Это означает, что время выполнения операции будет снижено практически в два раза. Структура последовательного быстрого алгоритма ДПФ такова, что при использовании такого представления возможно полное разделение вычислений 40 по тому же принципу. В результате мы приходим к следующему параллельному алгоритму двумерного ДПФ с представлением данных в алгебре гиперкомплексных чисел. Шаг 1. Переход от исходного представления к новому (тривиально, так как входные данные вещественные, а корни - комплексные). Шаг 2. Распределение данных по двум процессорам. Шаг 3. Расчет преобразования с использованием алгоритмов типа КулиТьюки. Шаг 4. Реконструкция гиперкомплексного спектра. 41 3 Программная реализация алгоритмов Описанные в разделах 1 и 2 алгоритмы реализованы в виде нескольких программных модулей. Модули написаны на языке программирования Java и объединены в исследовательский программный комплекс. Для реализации параллельных алгоритмов БПФ использовались как возможности языка Java для распределения задачи между ядрами центрального процессора, так и технология Cuda для выполнения расчетов на видеокартах от компании NVidia. Для доступа к Cuda из классов, написанных на языке Java, используется JCuda (Java API for Cuda). При построении 3D-модели поверхности океана используются технологии OpenGL и JOGL (Java API for OpenGL). Далее будут приведены общие сведения об используемых технологиях, а также особенности программной реализации, как алгоритмов преобразования Фурье, так и всей программы построения 3D-модели поверхности воды в целом. Текст исходного кода модулей приводится в Приложении В. 3.1 Описание технологии Cuda CUDA (Compute Unified Device Architecture) – это технология от компании Nvidia, предназначенная для разработки приложений для массивно- параллельных вычислительных устройств (в первую очередь для GPU начиная с серии G80) [10]. Основными плюсами CUDA являются ее бесплатность, простота (программирование ведется на "расширенном С") и гибкость. CUDA является дальнейшим развитием GPGPU (General Purpose computation on GPU). Дело в том, что уже с самого начала GPU активно использовали параллельность (как вершины, так и отдельные фрагменты могут обрабатываться параллельно и независимо друг от друга, т.е. очень хорошо ложатся на параллельную архитектуру). 42 В архитектуре GPU появились отдельные вершинные и фрагментные процессоры, выполняющие соответствующие программы. Данные процессоры вначале были крайне просты - можно было выполнять лишь простейшие операции, практически полностью отсутствовало ветвление, и все процессоры одного типа одновременно выполняли одну и ту же команду (классическая SIMD-архитектура). В результате GPU фактически стало устройством, реализующим потоковую вычислительную модель (stream computing model) есть потоки входных и выходных данных, состоящие из одинаковых элементов, которые могут быть обработаны независимо друг от друга. Обработка элементов осуществляется ядром (kernel) (рисунок 9). Рисунок 9 – Потоковые вычисления Появление CUDA предложило для GPGPU простую и удобную модель. В этой модели GPU рассматривается как специализированное вычислительное устройство (называемое device), которое: является сопроцессором к CPU (называемому host); обладает собственной памятью (DRAM); обладает возможностью параллельного выполнения огромного количества отдельных нитей (threads). При этом между нитями на CPU и нитями на GPU есть принципиальные различия: нити на GPU обладают крайне "небольшой стоимостью" - их создание и управление требует минимальных ресурсов (в отличии от CPU); 43 для эффективной утилизации возможностей GPU нужно использовать многие тысячи отдельных нитей (для CPU обычно нужно не более 10-20 нитей). Сами программы пишутся на так называемом языке программирования "расширенном" C, при этом их параллельная часть (ядра) выполняется на GPU, а обычная часть – на CPU. CUDA автоматически осуществляет разделением частей и управлением их запуском. CUDA использует большое число отдельных нитей для вычислений, часто каждому вычисляемому элементу соответствует одна нить. Все нити группируются в иерархию – grid/block/thread(рисунок 10). Рисунок 10 –Иерархия нитей в CUDA Верхний уровень – grid – соответствует ядру и объединяет все нити выполняющие данное ядро. grid представляет собой одномерный или двухмерный массив блоков (block). Каждый блок (block) представляет из себя одно/двух/трехмерный массив нитей (threads). 44 При этом каждый блок представляет собой полностью независимый набор взаимодействующих между собой нитей, нити из разных блоков не могут между собой взаимодействовать. Однако, нити внутри блока могут взаимодействовать между собой (т.е. совместно решать подзадачу). Это возможно с помощью: общей памяти (shared memory); функции синхронизации всех нитей блока (__synchronize). Подобная иерархия довольно естественна – с одной стороны хочется иметь возможность взаимодействия между отдельными нитями, а с другой – чем больше таких нитей, тем выше оказывается цена подобного взаимодействия. Поэтому исходная задача (применение ядра к входным данным) разбивается на ряд подзадач, каждая из которых решается абсолютно независимо (т.е. никакого взаимодействия между подзадачами нет) и в произвольном порядке. Сама же подзадача решается при помощи набора взаимодействующих между собой нитей. С аппаратной точки зрения все нити разбиваются на так называемые warp'ы – блоки подряд идущих нитей, которые одновременно (физически) выполняются и могут взаимодействовать друг с другом. Каждый блок нитей разбивается на несколько warp'ов, размер warp'а для всех существующих сейчас GPU равен 32. Важным моментом является то, что нити фактически выполняют одну и ту же команду, но каждая со своими данными. Поэтому если внутри warp'а происходит ветвление (например, в результате выполнения оператора if), то все нити warp'а выполняют все возникающие при этом ветви. Поэтому крайне желательно уменьшить ветвление в пределах каждого отдельного warp'а. 45 Кроме иерархии нитей существует также несколько различных типов памяти. Быстродействие приложения очень сильно зависит от скорости работы с памятью. Именно поэтому в традиционных CPU большую часть кристалла занимают различные кэши, предназначенные для ускорения работы с. В CUDA для GPU существует несколько различных типов памяти, доступных нитям, сильно различающихся между собой (таблица 2) [11]. Таблица 2 – Типы памяти в CUDA Тип памяти Доступ Уровень выделения Скорость работы регистры (registers) R/W per-thread высокая (on chip) local R/W per-thread низкая (DRAM) shared R/W per-block высокая (on-chip) global R/W per-grid низкая(DRAM) constant R/O per-grid высокая(on chip L1 cache) texture R/O per-grid высокая(on chip L1 cache) При этом CPU имеет R/W доступ только к глобальной, константной и текстурной памяти (находящейся в DRAM GPU) и только через функции копирования памяти между CPU и GPU (предоставляемые CUDA API). 3.2 Описание библиотеки JCuda Библиотека JCuda является проектом с открытым исходным кодом, направленным на предоставление удобного интерфейса взаимодействия CUDA и языка Java. Доступ к CUDA API реализован посредством JNI. При использовании JNI, программисту достаточно объявить определенные функции на C, которые реализованы вне Java как native. Native функции должны быть отдельно скомпилированы в платформо-зависимый двоичный код. На рисунке 11 изображена типичная схема «ручного» взаимодействия Java и СUDA посредством JNI [12]. 46 Рисунок 11 – Реализация доступа к Cuda через JNI Она включает в себя написание Java кода и промежуточного кода JNI на С для исполнения на центральном процессоре, а также CUDA C кода для исполнения на видеокарте. Промежуточный код также должен заниматься выделением и освобождением памяти и передачей данных между основной памятью и видеопамятью. Такой многоэтапный процесс непрозрачен и может вызвать ошибки, и было бы удобней как-либо автоматизировать этот процесс. Библиотека JCuda реализует похожую схему, с тем исключением, что JNI доступ к функциям драйвера c поддержкой CUDA уже реализован в готовых статических методах классов JCuda и JCudaDriver. Библиотека также предоставляет вспомогательные классы Pointer и Sizeof для обхода сложностей взаимодействия Java и C программ. Например, класс Pointer эмулирует реальный указатель памяти. Такой класс как LibUtils предоставляет служебные функции для определения разрядности платформы и операционной системы (эти служебные функции необходимы при взаимодействии с видеодрайвером Nvidia). Пакет jcuda состоит из двух подпакетов: jcuda.driver и jcuda.runtime, предоставляющие доступ к CUDA Driver API и CUDA Runtime API. Эти пакеты по сути содержат классы, копирующие структуру данных CUDA и методы, по 47 синтаксису совершенно аналогичные процедурам, предоставляемых CUDA API для С/С++ приложений. По причине невысокой абстракции от CUDA API для С/С++ приложений, Java код, написанный c использованием JCuda сильно похож на С код, написанный с использованием CUDA. Также в Java невозможно прямое написание вычислительных ядер (kernels) и их вызов, поэтому возникает необходимость писать CUDA C код отдельно. 3.3 Исследовательский программный комплекс формирования модели водной поверхности В разделе 1 приводилось описание того, как строится поверхность. В этом разделе будет рассмотрен алгоритм построения, а его также программная реализация. Программный комплекс состоит из следующих модулей: ssau.knyazev.ocean.Ocean – основной исполняемый класс; ssau.knyazev.ocean.OceanConst – интерфейс с константами; ocean.cu – файл с исходный кодом программы, исполняемой на GPU устройстве. В нем реализованы методы для генерации спектра в момент времени 𝑡 и расчета координат вершин, используемые при построении модели; один из подключаемых модулей для выполнения преобразования Фурье (в зависимости от выбранного алгоритма). Ниже для наглядности приведена схема работы комплекса в целом (рисунок 8), а также краткое описание каждого из блоков. 48 Инициализация Генерация начального спектра водной поверхности – ℎ̃0 Замер времени 1 Новый кадр Генерация спектра водной поверхности в момент времени t – ℎ̃(𝑡) Обратное преобразование Фурье над спектром водной поверхности ℎ̃(𝑡) Построение 3D-модели водной поверхности Замер времени 2 Завершение работы Рисунок 8 – Схема работы программного комплекса Приведем описание блоков схемы: 1. «Инициализация» – создание контекста Cuda и установление связи между Cuda и OpenGL. Создание VBO-объектов для последующего построения 3D-модели. Используемые методы: private void initShaders(GL2 gl); private void initJCuda(); private void initVBO(GL2 gl); 2. «Замер времени 1» – замер текущего времени. 49 3. ̃𝟎» – «Генерация начального спектра водной поверхности – 𝒉 Расчет спектра Филипса и наложение на него шума. Используемые методы: private float[] generate_h0(float[] h0); private float phillips(float Kx, float Ky, float Vdir, float V, float A, float dir_depend); 4. «Новый кадр» – начало расчета очередного кадра анимации поверхности воды. 5. «Генерация спектра водной поверхности в момент времени t – ̃ (𝒕)» – расчет спектра водной поверхности в момент времени. Выполняется на 𝒉 GPU устройстве в модуле ocean.cu. Используемый метод: extern "C" __global__ void generateSpectrumKernel(float2* h0, float2 *ht, unsigned int in_width, unsigned int out_width, unsigned int out_height, float t, float patchSize); 6. «Обратное преобразование Фурье над спектром водной ̃ (𝒕)» – обратное двумерное ДПФ над спектром водной поверхности 𝒉 поверхности, полученным в пункте 4. Выполняется на GPU устройстве в одном из модулей, в зависимости от выбранного алгоритма: fft-module.cu, fft-b2module.cu и fft-c-module.cu. 7. «Построение 3D-модели водной поверхности» – построение 3D- модели водной поверхности по расчитанной карте высот. Далее, если программа еще не завершает свое выполнение, то переходим к пункту 4. Используемые методы: public void display(GLAutoDrawable drawable); 50 extern "C" __global__ void updateHeightmapKernel(float* heightMap, float2* ht, unsigned int width); extern "C" __global__ void calculateSlopeKernel(float* h, float2 *slopeOut, unsigned int width, unsigned int height); 8. «Замер времени 2» – замер текущего времени. 9. «Завершение работы» – подсчет времени работы программы и вывод результатов. Программа завершает свою работу после выполнения некоторого числа кадров. В данной работе количество кадров равно 1000. Время работы программы вычисляется как разность значений полученных с помощью «Замер времени 2» и «Замер времени 1». А средний показатель числа кадров в секунду равен частному общему количеству кадров на затраченное время. Далее будут представлены программные реализации алгоритмов дискретного преобразования Фурье, описание которых приведено в разделе 2. 3.4 Общие сведения для алгоритмов двумерного случая Дискретное преобразование Фурье выполняется в разных алгебрах. Поэтому необходимо было ввести классы-обертки над числами каждой из алгебр. Обычные комплексные ssau.knyazev.core.Complex, числа представлены гиперкомплексные числа классом-оберткой оборачиваются ssau.knyazev.core.CComplex и наконец, прямая сумма комплексных чисел представлена типом ssau.knyazev.core.B2Complex. В процессе вычисления ДПФ основными операциями являются сложение чисел в соответсвующей алгебре и умножение на степени корней 𝜔1 , 𝜔2 . Для того, чтобы не вычислять синус и косинус для каждого набора коэффициентов 𝑚1 𝑛1 и 𝑚2 𝑛2 , , вычислим значения этих функций заранее и поместим в вектора, 51 а дальше будем просто высчитывать индекс используемого поворотного коэффициента. Также был написан модуль для генерации случайных чисел ssau.knyazev.modules.Generator, в котором реализованы методы для создания вектора или матрицы состоящей из случайных чисел, из выбранного диапазона. Текст исходного кода программных модулей приведен в Приложении В. 3.5 Программная реализация последовательного алгоритма двумерного ДПФ В данном подразделе будет рассмотрена реализация последовательного алгоритма двумерного ДПФ, описанного в подразделе 2.4.2. Реализовано в классах ssau.knyazev.fft.modules.oob.FFTModule для случая работы программы над матрицей отсчетов в виде классов-оберток над комплексными числами и ssau.knyazev.fft.modules.real.RealFFTModule в случае работы над матрицей вещественных чисел. Причем обрабатываемая матрица в таком случае будет состоять из 𝑁 строк и 2𝑁 столбцов, где 2 ∙ 𝑖 – индекс действительной части -го отсчета, а 2 ∙ 𝑖 + 1 –индекс мнимой части комплексного числа. Выполняется методами: public static Complex[][] FFTModule.fft2D(float[][] src); public static Complex[][] FFTModule.fft2D(float[][] src, 0); public static Complex[][] FFTModule.fft2D(Complex[][] src); public static Complex[][] FFTModule.inverseFFT2D(Complex[][] src); public static Complex[][] FFTModule.fft2D(Complex[][] src, 0); public static Complex[][] FFTModule.fft2D(Complex[][] src, 0, boolean inverse); Если преобразование Фурье вычисляется с применением специальных алгебр, то исполняемыми 52 классами будут и ssau.knyazev.fft.modules.oob.B2FFTModule ssau.knyazev.fft.modules.oob.CFFTModule при использовании прямой суммы комплексных чисел и гиперкомплексных чисел соответственно. Если расчеты ведутся без применения классов-оберток, а только лишь с примитивными типами float, то исполняемыми классами будут и ssau.knyazev.fft.modules.real.RealB2FFTModule ssau.knyazev.fft.modules.real.RealCFFTModule. Сам алгоритм для наглядности представим в виде схемы изображенной на рисунке 12. Инициализация Замер времени 1 Перестановка Преобразование в четырехмерную алгебру ДПФ Замер времени 2 Завершение работы Рисунок 12 – Схема последовательного алгоритма по схеме Кули-Тьюки Приведем описание блоков схемы: 1. «Инициализация» – создание и заполнение матрицы случайными числами (генерируется методами класса ssau.knyazev.modules.Generator). Если в качестве параметра передаются вещественные числа типа float, то они 53 оборачивается классом-оберткой заполняется вектор И ssau.knyazev.core.Complex. поворотных коэффициентов, наконец, используемый непосредственно при преобразовании. 2. «Замер времени 1» – замер текущего времени. 3. «Перестановка» – производится перестановка строк и столбцов матрицы, полученной в пунке 1, в порядке двоичной инверсии. 4. «Перевод в четырехмерную алгебру» - в случае, если преобразование Фурье выполняется с помощью четырехмерных алгебр (гиперкомплексные числа или прямая сумма комплексных чисел), то исходная матрица погружается пространство гиперкомплексных чисел или прямую сумму комплексных чисел. 5. «ДПФ» – дискретное двумерное преобразование Фурье матрицы. 6. «Замер времени 2» – замер текущего времени. 7. «Завершение работы» – подсчет времени работы программы и вывод результатов. Время работы программы вычисляется как разность значений, полученных при замере времени. 3.6 Программная реализация последовательного построчно-столбцового алгоритма двумерного ДПФ В данном подразделе будет рассмотрена реализация последовательного алгоритма двумерного ДПФ, описанного в подразделе 2.4.1. Реализовано в классе ssau.knyazev.fft.modules.oob.FFTModule. Выполняется методами: public static Complex[][] FFTModule.fft2D(float[][] src, 1); public static Complex[][] FFTModule.fft2D(Complex[][] src, 1); public static Complex[][] FFTModule.fft2D(Complex[][] src, 1, boolean inverse); 54 protected static Complex[][] fft2DRowCol(Complex[][] src). В построчно-столбцовом алгоритме используются одномерные преобразования Фурье. Методы, которые выполняют данное преобразование, перечислены ниже: public static Complex[] fft1D(Complex[] src); public static Complex[] inverseFFT1D(Complex[] src); public static Complex[] fft1D(Complex[] src, boolean inverse). Сам алгоритм для наглядности представлен в виде схемы, изображенной на рисунке 13. Инициализация Замер времени 1 Перестановка ДПФ каждой строки ДПФ каждого столбца Замер времени 2 Завершение работы Рисунок 13 – Схема последовательный построчно-столбцового алгоритма ДПФ Приведем описание блоков схемы: 1. «Инициализация» – создание и заполнение матрицы случайными числами (генерируется методами класса ssau.knyazev.modules.Generator). Если в качестве параметра передаются вещественные числа типа float, то они 55 оборачивается классом-оберткой заполняется вектор И ssau.knyazev.core.Complex. поворотных коэффициентов, наконец, используемый непосредственно при преобразовании. 2. «Замер времени 1» – замер текущего времени. 3. «Перестановка» – производится перестановка строк и столбцов матрицы, полученной в пунке 1, в порядке двоичной инверсии. 4. «ДПФ каждой строки» – дискретное одномерное преобразование Фурье над всеми строками матрицы отсчетов. 5. «ДПФ каждого столбца» – дискретное одномерное преобразование Фурье над всеми столбцами матрицы отсчетов. 6. «Замер времени 2» – замер текущего времени. 7. «Завершение работы» – подсчет времени работы программы и вывод результатов. Время работы программы вычисляется как разность значений, полученных при замере времени. 3.7 Программная реализация алгоритма двумерного ДПФ с распараллеливанием за счет структуры алгебры В данном подразделе будет рассмотрена реализация алгоритма двумерного ДПФ с распараллеливанием за счет структуры алгебры, описанным в подразделе 2.5.3. Реализовано в классе ssau.knyazev.fft.modules.CPUFFTModule. Выполняется методом: public static Complex[][] fft2DB2Complex(float[][] src). Сам алгоритм для наглядности представлен в виде схемы, изображенной на рисунке 14. 56 Инициализация Замер времени 1 Перестановка Преобразование в алгебру Инициализация процесса 1 Инициализация процесса 2 ДПФ 1 ДПФ 2 Синхронизация процессов Замер времени 2 Завершение работы Рисунок 14 – Алгоритм двумерного ДПФ с распараллеливанием за счет структуры алгебры Приведем описание блоков схемы: 1. числами «Инициализация» – создание и заполнение матрицы случайными (генерируется Заполняется вектор методами класса поворотных ssau.knyazev.modules.Generator). коэффициентов, используемый непосредственно при преобразовании. 2. «Замер времени 1» – замер текущего времени. 3. «Перестановка» – производится перестановка строк и столбцов матрицы, полученной в пунке 1, в порядке двоичной инверсии. 57 «Преобразование в алгебру» – если исходная матрица представлена 4. в виде вещественных чисел, то происходит ее преобразование в матрицу, состоящую из чисел типа B2Comlex. «Инициализация 5. процесса 1» – создание процесса с 2» – создание процесса с идентификационным номером 1. «Инициализация 6. процесса идентификационным номером 2. «ДПФ 7. 1» – процесс 1 выполняет дискретное двумерное преобразование Фурье матрицы, оперируя с коэффициентами 𝑢0 и 𝑢2. «ДПФ 8. 2» – процесс 2 выполняет дискретное двумерное преобразование Фурье матрицы, оперируя с коэффициентами 𝑢1 и 𝑢3. «Синхронизация процессов» – синхронизируется завершение работы 9. обоих процессов. 10. «Замер времени 2» – замер текущего времени. 11. «Завершение работы» – подсчет времени работы программы и вывод результатов. Время работы программы вычисляется как разность значений, полученных при замере времени. 3.8 Программная реализация алгоритма двумерного ДПФ с распараллеливанием по структуре декомпозиции В данном подразделе будет рассмотрена реализация алгоритма двумерного ДПФ с распараллеливанием по структуре декомпозиции, описанным в подразделе 2.5.2. Реализовано в классе ssau.knyazev.fft.modules.CPUFFTModule. Выполняется методом: public static Complex[][] fft2DByDecomposition(float[][] src, int Algebratype). 58 Сам алгоритм для наглядности представлен в виде схемы, изображенной на рисунке 15. Инициализация Замер времени 1 Перестановка Преобразование в алгебру Инициализация процесса (0, 0) Инициализация процесса (0, 1) Инициализация процесса (1, 0) Инициализация процесса (1, 1) ДПФ над 𝑀00 ДПФ над 𝑀01 ДПФ над 𝑀10 ДПФ над 𝑀11 Синхронизация процессов Замер времени 2 Завершение работы Рисунок 15 – Алгоритм двумерного ДПФ с распараллеливанием по структуре декомпозиции Приведем описание блоков схемы: 1. числами «Инициализация» – создание и заполнение матрицы случайными (генерируется Заполняется вектор методами класса поворотных ssau.knyazev.modules.Generator). коэффициентов, непосредственно при преобразовании. 2. «Замер времени 1» – замер текущего времени. 59 используемый 3. «Перестановка» – производится перестановка строк и столбцов матрицы, полученной в пунке 1, в порядке двоичной инверсии. 4. «Преобразование в алгебру» – если исходная матрица представлена в виде вещественных чисел, то происходит ее преобразование в матрицу, состоящую из чисел типа Complex, CComplex, B2Complex (в зависимости от выбранной алгебры). 5. «Инициализация процесса (0, 0)» – создание процесса с (1, 0)» – создание процесса с (0, 1)» – создание процесса с (1, 1)» – создание процесса с идентификационным номером (0, 0). 6. «Инициализация процесса идентификационным номером (1, 0). 7. «Инициализация процесса идентификационным номером (0, 1). 8. «Инициализация процесса идентификационным номером (1, 1). 9. «ДПФ над 𝑴𝟎𝟎 » – процесс (0, 0) выполняет дискретное двумерное 𝑁 преобразование Фурье над отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [0; − 1] , 𝑗 ∈ 2 𝑁 [0; − 1]. 2 10. «ДПФ над 𝑴𝟏𝟎 » – процесс (1, 0) выполняет дискретное двумерное 𝑁 𝑁 2 2 преобразование Фурье над отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [ ; 𝑁] , 𝑗 ∈ [0; − 𝑁]. 11. «ДПФ над 𝑴𝟎𝟏 » – процесс (0, 1) выполняет дискретное двумерное 𝑁 преобразование Фурье над отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [0; − 1] , 𝑗 ∈ 2 𝑁 [ − 1; 𝑁]. 2 12. «ДПФ над 𝑴𝟏𝟏 » – процесс (1, 1) выполняет дискретное двумерное 𝑁 преобразование Фурье над отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [ − 1; 𝑁] , 𝑗 ∈ 2 𝑁 [ − 1; 𝑁]. 2 60 13. «Синхронизация процессов» – синхронизируется завершение работы всех процессов. 14. «Замер времени 2» – замер текущего времени. 15. «Завершение работы» – подсчет времени работы программы и вывод результатов. Время работы программы вычисляется как разность значений, полученных при замере времени. 3.9 Программная двумерного ДПФ реализация с гибридного распараллеливанием по алгоритма структуре декомпозиции и распараллеливанием по структуре алгебры В данном подразделе будет рассмотрена реализация алгоритма двумерного ДПФ с распараллеливанием по структуре декомпозиции и распараллеливанием по структуре алгебры. Реализовано в классе ssau.knyazev.fft.modules.CPUFFTModule. Выполняется методом: public static Complex[][] fft2DB2ComplexByDnA(float[][] src). Сам алгоритм для наглядности представлен в виде схемы, изображенной на рисунке 16. 61 Инициализация Замер времени 1 Перестановка Преобразование в алгебру Инициализация процесса 1 (0, 0) Инициализация процесса 2 (0, 0) … Инициализация процесса 1 (1, 1) Инициализация процесса 2 (1, 1) ДПФ 1 над 𝑀00 ДПФ 2 над 𝑀00 … ДПФ 1 над 𝑀11 ДПФ 2 над 𝑀11 Синхронизация процессов Замер времени 2 Завершение работы Рисунок 16 – Схема гибридного алгоритма двумерного ДПФ Приведем описание блоков схемы: 1. числами «Инициализация» – создание и заполнение матрицы случайными (генерируется Заполняется вектор методами класса поворотных ssau.knyazev.modules.Generator). коэффициентов, используемые непосредственно при преобразовании. 2. «Замер времени 1» – замер текущего времени. 3. «Перестановка» – производится перестановка строк и столбцов матрицы, полученной в пунке 1, в порядке двоичной инверсии. 62 4. «Преобразование в алгебру» – если исходная матрица представлена в виде вещественных чисел, то происходит ее преобразование в матрицу, состоящую из чисел типа B2Complex. 5. «Инициализация процесса 1 (0, 0)» – создание процесса с идентификационным номером 1 (0, 0). 6. «Инициализация процесса 2 (0, 0)» – создание процесса с идентификационным номером 2 (0, 0). 7. «Инициализация процесса 1 (1, 1)» – создание процесса с идентификационным номером 1 (1, 1). 8. «Инициализация процесса 2 (1, 1)» – создание процесса с идентификационным номером 2 (1, 1). 9. «ДПФ 1 над 𝑴𝟎𝟎 » – процесс 1 (0, 0) выполняет дискретное двумерное преобразование Фурье, оперируя с коэффициентами 𝑢0 и 𝑢2, над 𝑁 𝑁 2 2 отсчетами матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [0; − 1] , 𝑗 ∈ [0; − 1]. 10. «ДПФ 2 над 𝑴𝟎𝟎 » – процесс 2 (0, 0) выполняет дискретное двумерное преобразование Фурье, оперируя с коэффициентами 𝑢1 и 𝑢3, над отсчетами 𝑁 𝑁 2 2 матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [0; − 1] , 𝑗 ∈ [0; − 1]. 11. «ДПФ 1 над 𝑴𝟏𝟏 » – процесс 1 (1, 1) выполняет дискретное двумерное преобразование Фурье, оперируя с коэффициентами 𝑢0 и 𝑢2, над отсчетами 𝑁 𝑁 2 2 матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [ − 1; 𝑁] , 𝑗 ∈ [ − 1; 𝑁]. 12. «ДПФ 2 над 𝑴𝟏𝟏 » – процесс 2 (1, 1) выполняет дискретное двумерное преобразование Фурье, оперируя с коэффициентами 𝑢1 и 𝑢3, над отсчетами 𝑁 𝑁 2 2 матрицы 𝑋𝑖,𝑗 , где 𝑖 ∈ [ − 1; 𝑁] , 𝑗 ∈ [ − 1; 𝑁]. 13. «Синхронизация процессов» – синхронизируется завершение работы всех процессов. 14. «Замер времени 2» – замер текущего времени. 63 15. «Завершение работы» – подсчет времени работы программы и вывод результатов. Время работы программы вычисляется как разность значений, полученных при замере времени. В такой реализации участвуют 8 процессов. Пропущенные процессы процесс 1 (1, 0), процесс 2 (1, 0), процесс 1 (0, 1), процесс 2 (0, 1) выполняются аналогично пунктам 5-10. 3.10 Программная реализация ДПФ с постоянной синхронизацией процессов и без В предыдущих реализациях, процессы работают независимо друг от друга, синхронизируясь лишь на последней итерации. В данном разделе будет приведена схема с постоянной синхронизацией процессов на каждом шаге. Такая реализация окажется крайне удачной и будет реализована с помощью связки технологий Cuda + JCuda. Реализовано в классе ssau.knyazev.fft.modules.CPUFFTModule. А также с технологией Cuda в классе ssau.knyazev.fft.modules.GPUFFTModule, исходный код исполняемый на видеокарте: fft-module.cu, fft-b2-module.cu и fft-c-module.cu. Выполняется методами класса ssau.knyazev.fft.modules.CPUFFTModule: public static float[][] fft2DComplexWithSync(float[][] src); public static float [][] fft2DCComplexWithSync(float[][] src); public static float [][] fft2DB2ComplexWithSync(float[][] src); и методами класса ssau.knyazev.fft.modules.GPUFFTModule: public static float[][] fft2DComplex(float[][] src); public static void fft2DComplex(CUdeviceptr src); public static float [][] fft2DCComplex(float[][] src); public static void fft2DCComplex(CUdeviceptr src); public static float [][] fft2DB2Complex(float[][] src); public static void fft2DB2Complex(CUdeviceptr src). 64 На рисунке 17 показан пример выполнения 4 процессов. Инициализация Замер времени 1 Перестановка Преобразование в алгебру Инициализация процесса (0, 0) Инициализация процесса (0, 1) Инициализация процесса (1, 0) Инициализация процесса (1, 1) Следующая итерация Бабочка над 0, 4, 8, … , 𝑁 − 4 четверкой отсчетов Бабочка над 1, 5, 9, … , 𝑁 − 3 четверкой отсчетов Бабочка над 2, 6, 10, … , 𝑁 − 2 четверкой отсчетов Бабочка над 3, 7, 11, … , 𝑁 − 1 четверкой отсчетов Синхронизация процессов Замер времени 2 Завершение работы Рисунок 17 – Алгоритм двумерного ДПФ по схеме Кули-Тьюки с постоянной синхронизацией процессов Приведем описание блоков схемы: 1. числами «Инициализация» – создание и заполнение матрицы случайными (генерируется методами класса 65 ssau.knyazev.modules.Generator). Заполняется вектор поворотных коэффициентов, используемые непосредственно при преобразовании. 2. «Замер времени 1» – замер текущего времени. 3. «Перестановка» – производится перестановка строк и столбцов матрицы, полученной в пунке 1, в порядке двоичной инверсии. 4. «Преобразование в алгебру» – если исходная матрица представлена в виде вещественных чисел, то происходит ее преобразование в матрицу, состоящую из чисел типа Complex, CComplex, B2Complex (в зависимости от выбранной алгебры). 5. «Инициализация процесса (0, 0)» – создание процесса с (1, 0)» – создание процесса с (0, 1)» – создание процесса с (1, 1)» – создание процесса с идентификационным номером (0, 0). 6. «Инициализация процесса идентификационным номером (1, 0). 7. «Инициализация процесса идентификационным номером (0, 1). 8. «Инициализация процесса идентификационным номером (1, 1). 9. «Новая итерация» – Очередная итерация в преобразовании Фурье. 10. «Бабочка над 𝟎, 𝟒, 𝟖, … , 𝑵 − 𝟒 четверкой отсчетов» – процесс (0, 0) производит умножение и сложение с 0, 4, 8, … , 𝑁 − 4 четверкой отсчетов. 11. «Бабочка над 𝟏, 𝟓, 𝟗, … , 𝑵 − 𝟑 четверкой отсчетов» – процесс (1, 0) производит умножение и сложение с 1, 5, 9, … , 𝑁 − 3 четверкой отсчетов. 12. «Бабочка над 𝟐, 𝟔, 𝟏𝟎, … , 𝑵 − 𝟐 четверкой отсчетов» – процесс (0, 1) производит умножение и сложение с 2, 6, 10, … , 𝑁 − 2 четверкой отсчетов. 13. «Бабочка над 𝟑, 𝟕, 𝟏𝟏, … , 𝑵 − 𝟏 четверкой отсчетов» – процесс (1, 1) производит умножение и сложение с 3, 7, 11, … , 𝑁 − 1 четверкой отсчетов. 14. «Синхронизация процессов» – синхронизируется завершение работы всех процессов. Если все итерации пройдены, то программа завершает свое выполнение, иначе возвращаемся к пункту 9. 15. «Замер времени 2» – замер текущего времени. 66 16. «Завершение работы» – подсчет времени работы программы и вывод результатов. Время работы программы вычисляется как разность значений, полученных при замере времени. 67 4 Экспериментальные исследования алгоритмов и анализ результатов При помощи программного комплекса, разработанного в процессе выполнения работы, были проведены серии экспериментов. Исследования проводились как над реализованными алгоритмами БПФ, так и над программным комплексом в целом. В свою очередь, исследование алгоритмов БПФ разделены на две части: в первой части реализованные алгоритмы тестировались на многоядерном центральном процессоре, а во второй части на графическом процессоре. В качестве входных данных использовалась квадратная матрица, состоящая из вещественных чисел, генерируемых случайным образом. В результате были получены значения времени, затрачиваемое на вычисление преобразования Фурье при помощи определенного алгоритма, а также значение скорости прорисовки анимации 3D-модели. Программный комплекс тестировался на системе конфигурацией: процессор: AMD Phenom II X4 965 @ 3.40 Ghz; оперативная память: 4 Gb; видеокарта: NVidia GeForce 450 GTS 1 Gb. Используемое программное обеспечение: операционная система: Windows 7 Максимальная; NVIDIA DevDriver 301.32; CudaToolkit 4.1.28; NVIDIA GPU Computing SDK 4.1; JDK 1.7.0; JCuda 0.4.1. 68 со следующей 4.1 Исследование скорости выполнения алгоритмов на CPU В данном разделе исследуется скорость выполнения алгоритмов ДПФ на центральном процессоре. Каждый из алгоритмов выполняется как последовательно (1 процесс), так и параллельно (2, 4 процесса). Построчно-столбцовый алгоритм (комплексные числа). Время выполнения алгоритма приводится в таблице 3. Для визуального представления также приведен график на рисунке 18. Таблица 3 - Время работы последовательного и параллельного построчно-столбцового алгоритма двумерного ДПФ Размер 1 процесс 2 процесса 4 процесса Время, мс Время, мс Время, мс 8 0,515228 0,59813 0,798626 16 0,74007 0,749365 0,925484 32 1,353672 1,117226 1,18085 64 4,226612 2,488438 2,973053 128 15,610465 8,072981 8,175732 256 74,563327 37,2345 35,121239 512 353,893261 184,948027 148,41198 1024 1517,954506 907,041146 630,813882 2048 9531,025981 5385,938665 3661,409248 4096 41395,98399 24099,89818 16595,41074 69 45000 40000 35000 30000 25000 20000 15000 10000 5000 0 0 500 1000 1500 2000 1 процесс 2500 2 процесса 3000 3500 4000 4500 4 процесса Рисунок 18 –Время выполнения построчно-столбцового алгоритма ДПФ (комплексные числа) Из таблицы 3 следует, что среднее значение ускорения параллельного построчно-столбцового алгоритма двумерного ДПФ относительного последовательного алгоритма равно 1,74 и 2,061 для 2 и 4 процессов соответственно. Алгоритм по схеме Кули-Тьюки (комплексные числа). Время выполнения алгоритма приводится в таблице 4. Для визуального представления также приведен график на рисунке 19. 70 Таблица 4 – Время работы последовательного и параллельного алгоритма двумерного ДПФ по схеме Кули-Тьюки Размер 1 процесс 2 процесса 4 процесса Время, мс Время, мс Время, мс 8 0,574773 0,584278 0,698873 16 0,784655 0,86327 0,848908 32 1,313255 1,114228 1,261474 64 3,654897 2,408893 2,651096 128 13,337874 7,773391 7,782655 256 62,847353 35,762575 33,891906 512 283,133216 164,77343 137,447949 1024 1274,520691 772,609426 622,614283 2048 6051,432017 3741,407473 2961,397645 4096 26932,53608 16671,89694 13248,04979 30000 25000 20000 15000 10000 5000 0 0 500 1000 1500 1 процесс 2000 2500 2 процесса 3000 3500 4000 4500 4 процесса Рисунок 19 – Время выполнения ДПФ по алгоритму Кули-Тьюки (комплексные числа) Из таблицы 4 следует, что среднее значение ускорения параллельного выполнения двумерного ДПФ по алгоритму Кули-Тьюки относительного 71 последовательного алгоритма равно 1,5962 и 1,7714 для 2 и 4 процессов соответственно. Алгоритм по схеме Кули-Тьюки с постоянной синхронизацией процессов (комплексные числа). Время выполнения алгоритма приводится в таблице 5. Для визуального представления также приведен график на рисунке 19. Таблица 5 – Время работы последовательного и параллельного алгоритма двумерного ДПФ по схеме Кули-Тьюки с постоянной синхронизацией процессов Размер 1 процесс 2 - процесса 4 - процесса Время, мс Время, мс Время, мс 8 0,533667 0,585717 0,737821 16 0,787503 0,876792 0,86309 32 1,343118 1,12868 1,135425 64 3,61352 2,372824 3,179996 128 13,669096 7,691747 7,785174 256 64,01483 35,239492 31,747222 512 283,918321 165,735794 130,596022 1024 1271,905845 773,191695 589,442288 2048 6021,031391 3737,298544 2732,80889 4096 26896,6452 16730,82045 12835,36278 72 30000 25000 20000 15000 10000 5000 0 0 500 1000 1500 1 процесс 2000 2500 2 процесса 3000 3500 4000 4500 4 процесса Рисунок 20 – Время выполнения ДПФ по схеме Кули-Тьюки с постоянной синхронизацией процессов (комплексные числа) Из таблицы № следует, что среднее значение ускорения параллельного выполнения двумерного ДПФ по алгоритму Кули-Тьюки с постоянной синхронизацией относительного последовательного алгоритма равно 1,6008 и 1,8302 для 2 и 4 процессов соответственно. Алгоритм по схеме Кули-Тьюки (алгебра гиперкомплексных чисел). Время выполнения алгоритма приводится в таблице 6. Для визуального представления также приведен график на рисунке 21. 73 Таблица 6 – Время работы последовательного и параллельного алгоритма двумерного ДПФ по схеме Кули-Тьюки Размер 1 процесс 2 процесса 4 процесса Время, мс Время, мс Время, мс 8 0,672494 0,68002 0,734259 16 0,942043 0,733318 0,98477 32 1,464712 1,418657 1,371704 64 3,95563 3,24077 3,012267 128 14,566704 11,643757 9,663452 256 68,958366 44,670131 38,750327 512 330,078631 219,578165 184,338636 1024 1373,240515 955,676984 748,661262 2048 5952,702585 4513,459392 3608,435308 4096 33317,90015 17002,58979 14112,00352 35000 30000 25000 20000 15000 10000 5000 0 0 500 1000 1500 1 процесс 2000 2500 2 процесса 3000 3500 4 процесса Рисунок 21 – Время выполнения ДПФ по алгоритму Кули-Тьюки (алгебра гиперкомплексных чисел) 74 4000 4500 Из таблицы 6 следует, что среднее значение ускорения параллельного выполнения двумерного ДПФ по алгоритму Кули-Тьюки относительного последовательного алгоритма равна 1,4083 и 1,6689 для 2 и 4 процессов соответственно. Алгоритм по схеме Кули-Тьюки с учетом симметрии спектра (алгебра гиперкомплексных чисел). Время выполнения быстрого алгоритма приводится в таблице 7. Для визуального представления также приведен график на рисунке 22, на котором помимо табличных данных из таблицы 7 показан график времени выполнения ГДПФ без учета симметрии на 4 процессах. Таблица 7 – Время работы последовательного и параллельного алгоритма двумерного ДПФ по схеме Кули-Тьюки с учетом симметрии спектра Размер 1 процесс 2 процесса 4 процесса Время, мс Время, мс Время, мс 8 0,600861 0,671212 0,744267 16 0,829938 0,829874 0,81775 32 1,392253 1,168565 1,025924 64 3,016298 2,470778 2,150192 128 10,040015 7,642538 6,282182 256 43,224153 29,319883 25,864024 512 221,934934 144,591515 125,216848 1024 907,133965 634,799305 493,813375 2048 4183,207224 2653,857531 2162,185734 4096 20753,24972 14756,45609 10619,13472 75 25000 20000 15000 10000 5000 0 0 500 1000 1 процесс 1500 2000 2 процесса 2500 3000 4 процесса 3500 4000 4500 ГДПФ (4 процесса) Рисунок 22 – Время выполнения алгоритма ДПФ по алгоритму Кули-Тьюки с учетом симметрии спектра (алгебра гиперкомплексных чисел) Алгоритм по схеме Кули-Тьюки (прямая сумма комплексных алгебр). Время выполнения быстрого ДПФ по алгоритму Кули-Тьюки над отсчетами в прямой сумме комплексных алгебр приводится в таблице 8. Также в таблицу внесены результаты с распараллеливанием по структуре алгебры на 2 процесса. Для визуального представления также приведен график на рисунке 23. Таблица 8 – Время работы последовательного и параллельного алгоритма двумерного ДПФ по схеме Кули-Тьюки 1 процесс Размер 8 16 32 64 128 256 512 1024 2048 4096 Время, мс 0,475891 0,954751 1,747174 5,732735 20,708356 91,163505 432,066458 1779,65201 8281,161808 39485,89506 ПСА - 2 процесса Время, мс 0,785402 1,097277 1,783587 5,256227 17,720785 63,876286 293,969508 1149,598828 5339,293824 25085,08769 76 2 процесса 4 процесса Время, мс 0,77104 1,082892 1,790819 4,703418 17,102388 63,542767 304,57695 1247,186494 5807,936654 27494,10498 Время, мс 0,832955 1,167414 2,060275 4,6156 14,008792 59,677042 270,78196 1040,823552 4851,677423 22987,15834 45000 40000 35000 30000 25000 20000 15000 10000 5000 0 0 500 1000 1 процесс 1500 2000 2 процесса 2500 3000 4 процесса 3500 4000 4500 2 процесса (ПСА) Рисунок 23 – Время выполнения ДПФ по алгоритму Кули-Тьюки (прямая сумма комплексных алгебр) Также для простоты сравнения эффективности алгоритмов между собой приведены графики времени их выполнения для 1, 2 и 4 процессов в Приложении Б на рисунках Б.1, Б.2 и Б.3. 4.2 Исследование скорости выполнения алгоритмов на GPU В данном разделе исследуется скорость выполнения алгоритмов ДПФ на видеокарте NVIDIA GeForce 450 GTS. Количество исполняемых потоков зависит от размеров входной матрицы, например, для матрицы размером 25 × 25 число потоков будет равным 210 . Однако, потоки – это логические процессы, число же физических процессов будет намного меньше, поскольку их количество не может быть больше числа исполняемых ядер на устройстве. На данной видеокарте расположено 192 ядра, поэтому и число одновременно исполняемых процессов будет равным 192. 77 Построчно-столбцовый алгоритм. Время выполнения построчно- столбцового алгоритма приводится в таблице 9. Для сравнения также приведены результаты, полученные в ходе исследования алгоритма на CPU. Таблица 9 – Время работы построчно-столбцового алгоритма двумерного ДПФ на GPU 1 процесс 2 процесса 4 процесса CUDA Размер 8 16 32 64 128 256 512 1024 2048 Время, мс 0,515228 0,74007 1,353672 4,226612 15,610465 74,563327 353,893261 1517,954506 9531,025981 Время, мс 0,59813 0,749365 1,117226 2,488438 8,072981 37,2345 184,948027 907,041146 5385,938665 Время, мс 0,798626 0,925484 1,18085 2,973053 8,175732 35,121239 148,41198 630,813882 3661,409248 Время, мс 0,427319 0,526832 0,701184 2,264892 8,492318 35,387474 140,480071 412,29942 3128,112019 4096 41395,98399 24099,89818 16595,41074 14142,51138 Алгоритм по схеме Кули-Тьюки с постоянной синхронизацией процессов (алгебра комплексных чисел). Время выполнения быстрого ДПФ по алгоритму Кули-Тьюки с постоянной синхронизацией приводится в таблице 10. Для сравнения также приведены результаты, полученные в ходе исследования алгоритма на CPU. Таблица 10 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки на GPU 1 процесс 2 процесса 4 процесса Размер 8 16 32 64 128 256 512 1024 2048 Время, мс 0,533667 0,787503 1,343118 3,61352 13,669096 64,01483 283,918321 1271,905845 6021,031391 Время, мс 0,585717 0,876792 1,12868 2,372824 7,691747 35,239492 165,735794 773,191695 3737,298544 Время, мс 0,737821 0,86309 1,135425 3,179996 7,785174 31,747222 130,596022 589,442288 2732,80889 Время, мс 0,632313 0,692099 0,827982 1,02722 1,302945 1,916428 3,563669 8,141996 26,37282 4096 26896,6452 16730,82045 12835,36278 114,044406 78 CUDA Алгоритм по схеме Кули-Тьюки с постоянной синхронизацией процессов (алгебра гиперкомплексных чисел). Время выполнения быстрого ДПФ по алгоритму Кули-Тьюки с постоянной синхронизацией приводится в таблице 11. Для сравнения также приведены результаты, полученные в ходе исследования алгоритма на CPU. Таблица 11 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки на GPU Размер 8 16 32 64 128 256 512 1024 2048 4096 1 процесс 2 процесса 4 процесса Время, мс 0,672494 0,942043 1,464712 3,95563 14,566704 68,958366 330,078631 1373,240515 5952,702585 33317,90015 Время, мс 0,68002 0,733318 1,418657 3,24077 11,643757 44,670131 219,578165 955,676984 4513,459392 17002,58979 Время, мс 0,734259 0,98477 1,371704 3,012267 9,663452 38,750327 184,338636 748,661262 3608,435308 14112,00352 CUDA Время, мс 0,755933 0,989016 1,172871 1,335468 1,487362 2,108847 2,73426 4,179646 9,608171 28,948367 Алгоритм по схеме Кули-Тьюки с учетом симметрии спектра (алгебра гиперкомплексных чисел). Время выполнения быстрого ДПФ по алгоритму Кули-Тьюки с постоянной синхронизацией приводится в таблице 12. Таблица 12 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки с учетом симметрии спектра на GPU Размер 8 16 32 64 128 256 512 1024 2048 4096 1 процесс 2 процесса 4 процесса Время, мс 0,600861 0,829938 1,392253 3,016298 10,040015 43,224153 221,934934 907,133965 4183,207224 20753,24972 Время, мс 0,671212 0,829874 1,168565 2,470778 7,642538 29,319883 144,591515 634,799305 2653,857531 14756,45609 Время, мс 0,744267 0,81775 1,025924 2,150192 6,282182 25,864024 125,216848 493,813375 2162,185734 10619,13472 79 CUDA Время, мс 0,740581 0,979901 0,882533 0,860976 1,09269 1,266064 1,794496 2,5438 5,222815 16,883298 Алгоритм по схеме Кули-Тьюки с постоянной синхронизацией процессов (прямая сумма комплексных алгебр). Время выполнения быстрого ДПФ по алгоритму Кули-Тьюки с постоянной синхронизацией приводится в таблице 13. Для сравнения также приведены результаты, полученные в ходе исследования алгоритма на CPU. Таблица 13 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки на GPU 1 процесс Размер 8 16 32 64 128 256 512 1024 2048 4096 ПСА - 2 процесса Время, мс 0,475891 0,954751 1,747174 5,732735 20,708356 91,163505 432,066458 1779,65201 8281,161808 39485,89506 2 процесса 4 процесса Время, мс 0,77104 1,082892 1,790819 4,703418 17,102388 63,542767 304,57695 1247,186494 5807,936654 27494,10498 Время, мс 0,832955 1,167414 2,060275 4,6156 14,008792 59,677042 270,78196 1040,823552 4851,677423 22987,15834 Время, мс 0,785402 1,097277 1,783587 5,256227 17,720785 63,876286 293,969508 1149,598828 5339,293824 25085,08769 CUDA Время, мс 0,732186 0,957537 1,150591 1,337417 1,511742 2,156865 2,775052 4,236497 9,330453 27,60724 Алгоритм по схеме Кули-Тьюки с учетом симметрии спектра (прямая сумма комплексных алгебр). Время выполнения быстрого ДПФ по алгоритму Кули-Тьюки с постоянной синхронизацией приводится в таблице 14. Для сравнения также приведены результаты, полученные в ходе исследования алгоритма на CPU. Таблица 14 – Время работы алгоритма двумерного ДПФ по схеме Кули-Тьюки с учетом симметрии спектра на GPU 1 процесс Размер 8 16 32 64 128 256 512 1024 2048 4096 Время, мс 0,475891 0,954751 1,747174 5,732735 20,708356 91,163505 432,066458 1779,65201 8281,161808 39485,89506 ПСА - 2 процесса Время, мс 0,785402 1,097277 1,783587 5,256227 17,720785 63,876286 293,969508 1149,598828 5339,293824 25085,08769 2 процесса 4 процесса Время, мс 0,77104 1,082892 1,790819 4,703418 17,102388 63,542767 304,57695 1247,186494 5807,936654 27494,10498 Время, мс 0,832955 1,167414 2,060275 4,6156 14,008792 59,677042 270,78196 1040,823552 4851,677423 22987,15834 80 CUDA Время, мс 0,740926 0,974491 0,872494 0,848601 1,071742 1,232811 1,739784 2,441934 4,927841 15,636526 Для сравнения эффективности алгоритмов ДПФ, реализованных с помощью технологии CUDA, на рисунке 24 приведена наглядная диаграмма. На диаграмме продемонстрировано время выполнения алгоритмов ДПФ над матрицей отсчетов размером: 1024 × 1024, 2048 × 2048 и 4096 × 4096. 30 25 20 15 10 5 0 1024x1024 2048x2048 4096x4096 Схема Кули-Тьюки (комплексные числа) Схема Кули-Тьюки (гиперкомплексные числа) Схема Кули-Тьюки + учет симметрии (гиперкомплексные числа) Схема Кули-Тьюки (прямая сумма комплексных алгебр) Схема Кули-Тьюки + учет симметрии (прямая сумма комплексных алгебр) Рисунок 24 – Сравнение эффективности алгоритмов ДПФ для матриц отсчетов размером 1024х1024, 2048х2048 и 4096х4096 4.3 Исследование скорости прорисовки модели водной поверхности В данном разделе исследуется прикладное применение быстрых алгоритмов ДПФ при построении 3D-модели водной поверхности. Для каждого из реализованных алгоритмов ДПФ с использованием технологии CUDA приводятся средние значения числа кадров в секунду для разных размеров 81 матриц спектра водной поверхности. В таблицу 15 сведены результаты для следующих алгоритмов: ПСА – построчно столбцовый алгоритм; КТ – ДПФ по схеме Кули-Тьюки (алгебра комплексных чисел); ГКТ – ДПФ по схеме Кули-Тьюки (алгебра гиперкомплексных чисел); ГКТ+симметрия – ДПФ по схеме Кули-Тьюки с учетом симметрии спектра (алгебра гиперкомплексных чисел); ПСКТ – ДПФ по схеме Кули-Тьюки (прямая сумма комплексных алгебр); ПСКТ+симметрия – ДПФ по схеме Кули-Тьюки с учетом симметрии спектра (прямая сумма комплексных алгебр). Таблица 15 – Скорость построения 3D-модели для каждого из алгоритмов ПСА КТ ГКТ ПСКТ FPS ГКТ+ симметрия FPS FPS ПСКТ+ симметрия FPS FPS FPS 8 550,8203367 494,9348369 466,3987672 469,7623378 471,6222532 469,6862167 16 524,7498255 482,8732115 422,3233358 423,9553422 428,0134824 424,9299609 32 476,5785473 464,7002103 389,1085407 438,6659467 392,5113544 440,6062742 64 272,2605686 439,8473554 364,4941841 440,715793 364,2354316 443,1325751 128 100,6049881 376,6197474 340,7260668 393,6640558 337,9190069 396,9373899 256 27,08067018 325,5091777 274,1167205 356,4683681 270,5555235 360,7445045 512 7,003114103 216,2027532 198,1058309 243,4249106 196,5177449 246,7106683 1024 1,620286981 121,1784409 110,4381135 134,7890733 109,7490522 136,6655492 2048 0,274505881 45,3293684 40,97461229 49,95004995 41,44624517 50,69701809 Размер Как видно из результатов, погружение входных данных в специальную алгебраическую структуру на размерах сетки 2048 × 2048 дает выигрыш в 4-5 кадров, тем самым давая более комфортный просмотр анимации. Ниже на рисунке 25 показан пример построенной 3D-модели для размера сетки спектра водной поверхности равной 2048 × 2048. Примеры работы программы для других размеров сетки приведены в приложении Б. 82 Рисунок 25 – Пример работы программы с сеткой размером 2048х2048 83 ЗАКЛЮЧЕНИЕ В данной работе были описаны быстрые алгоритмы дискретного преобразования Фурье, а также способы построения 3D-модели водной поверхности океана. Написаны программные модули, реализующие представленные алгоритмы ДПФ. Написан программный комплекс, строящий 3D-модель водной поверхности. Исследования показали, что погружение входных данных в специальную алгебраическую структуру позволяют выиграть в скорости вычисления ДПФ относительно вычисления ДПФ, выполняемого во множестве комплексных чисел, что в свою очередь ускоряет процесс построения 3D-модели водной поверхности на 10-15%. В результате проделанной работы сделаны следующие выводы: 1. Все алгоритмы, выполняемые по схеме Кули-Тьюки, оказываются значительно эффективнее построчно-столбцового алгоритма. Это особенно ярко выражено при вычислении на графическом процессоре, когда время работы снижается в 10-100 раз. 2. Исполнение алгоритмов по схеме Кули-Тьюки с постоянной синхронизацией оказывается на несколько миллисекунд быстрее (и этот показатель растет вместе с увеличением числа исполняемых процессов и/или размеров матрицы), чем независимое выполнение. Это связано с тем, что в асинхронном выполнении объединение матриц выполняет только один процесс, а остальные в это время простаивают. В свою очередь алгоритм с постоянной синхронизацией имеет два недостатка. Во-первых, синхронизация потоков на CPU является дорогостоящей операцией (на GPU потоки считаются «легкими» и затраты на их синхронизацию незначительны). Во-вторых, потоки вынуждены на каждом шаге итерации ждать завершения еще работающих потоков, таким образом, простаивая некоторое время. 84 3. Метод погружения входного сигнала в специальную алгебраическую структуру является мощным средством повышения производительности вычисления дискретного преобразования Фурье, а вместе с учетом симметрии спектра дает выигрыш при построении 3D-модели на 10-15 %. 85 СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ 1. Computer image processing // V.Soifer (ed), VDM Verlag Dr.Müller, Saarbrücken, Germany, 2010. 2. V.M. Chernov. Fast algorithms of Discrete Orthogonal Transforms for Data Represented in Cyclotomic Fields // Pattern Recognition and Image Analysis. 1993. Vol.3, No.4. P. 455-458. 3. Jerry Tessendorf. Simulating Ocean Water // SIGGRAPH course notes. 2004. 4. David Henry. On Gerstner’s Water Wave // Journal of Nonlinear Mathematical. Physics. 2008. Vol. 15. Supplement 2. P. 87-95. 5. Jason L. Mitchell. Real-Time Synthesis and Rendering of Ocean Water // ATI Research Technical Report. April 2005. 6. Chicheva M.A. Parallel implementation of multidimensional hypercomplex DFT with regard for real type of input signal // 9-th International Conference on Pattern Recognition and Image Analysis: New Information Technologies. (PRIA-92008). Nizhnii Novgorod, the Russian Federation Sptember 14-20, 2008. Conference proceedings v1, p. 70-73. 7. Чичева М.А. Компьютерная алгебра. Методические указания к курсовому проектированию: учебное пособие. Самара.: Изд-во Самар. гос. аэрокосм. ун-та, 2007. 64с. 8. Быстрое преобразование Фурье [Электронный ресурс]// dsplib.ru. Теория и практика цифровой обработки сигналов. URL: http://www.dsplib.ru/content/fft/fft.html (дата обращения: 14.04.2012). 9. Тропченко А.Ю., Тропченко А.А. Цифровая обработка сигналов. Методы предварительной обработки. Учебное пособие по дисциплине «Теоретическая информатика». СПб.: СПбГУ ИТМО, 2009. 100 с. 10. steps3D - Tutorials - Основы CUDA [Электронный ресурс]// steps3D Графика, ООП. URL: http://steps3d.narod.ru/tutorials/cuda-tutorial.html (дата 86 обращения: 14.04.2012). 11. CUDA Zone | NVIDIA Developer Zone [Электронный ресурс]// Мировой лидер в области технологий визуальных вычислений | NVIDIA. URL: http://developer.nvidia.com/category/zone/cuda-zone (дата обращения: 14.04.2012). 12. Yonghong Yan, Max Grossman, Vivek Sarkar. JCUDA: A ProgrammerFriendly Interface for Accelerating Java Programs with Cuda. Department of Computer Science. Rice University, 2009. 13p. 87 ПРИЛОЖЕНИЕ А Графики сравнения алгоритмов ДПФ 45000 40000 35000 30000 25000 20000 15000 10000 5000 0 0 500 1000 1500 2000 2500 3000 3500 4000 Построчно-столбцовый алгоритм Кули-Тьюки (комплексные числа) Кули-Тьюки с синхронизацией (комплексные числа) Кули-Тьюки (гиперкмоплексные числа) Кули-Тьюки с учетом симметрии спектра (гиперкомплексные числа) Кули-Тьюки (прямая сумма) Рисунок А.1 – Сравнение алгоритмов, выполняемых последовательно 88 4500 30000 25000 20000 15000 10000 5000 0 0 500 1000 1500 2000 2500 3000 3500 4000 Построчно-столбцовый алгоритм Кули-Тьюки (комплексные числа) Кули-Тьюки с синхронизацией (комплексные числа) Кули-Тьюки (гиперкмоплексные числа) Кули-Тьюки с учетом симметрии спектра (гиперкомплексные числа) Кули-Тьюки (пряммая сумма) Кули-Тьюки с распараллеливанием по структуре алгебры (пряммая сумма) Рисунок А.2 – Сравнение алгоритмов, выполняемых параллельно на 2 процессах 89 4500 25000 20000 15000 10000 5000 0 0 500 1000 1500 2000 2500 3000 3500 4000 4500 Построчно-столбцовый алгоритм Кули-Тьюки (комплексные числа) Кули-Тьюки с синхронизацией (комплексные числа) Кули-Тьюки (гиперкмоплексные числа) Кули-Тьюки с учетом симметрии спектра (гиперкомплексные числа) Кули-Тьюки (пряммая сумма) Рисунок А.3 – Сравнение алгоритмов ДПФ, выполняемых параллельно на 4 процессах 90 ПРИЛОЖЕНИЕ Б Примеры работы программы для разных размеров сетки Рисунок Б.1 – Пример работы программы с сеткой размером 512х512 91 Рисунок Б.2 – Пример работы программы с сеткой размером 128х128 92 ПРИЛОЖЕНИЕ В Исходный текст программы ssau.knyazev.ocean.OceanConst.java package ssau.knyazev.ocean; public interface OceanConst { String vertexShaderSource ="varying vec3 eyeSpacePos;\n" + "varying vec3 worldSpaceNormal;\n" + "varying vec3 eyeSpaceNormal;\n" + "uniform float heightScale; // = 0.5;\n" + "uniform float chopiness; // = 1.0;\n" + "uniform vec2 size; // = vec2(256.0, 256.0);\n" + "\n" + "void main()\n" + "{" + " float height = gl_MultiTexCoord0.x;\n" + " vec2 slope = gl_MultiTexCoord1.xy;\n" + "\n" + " // calculate surface normal from slope for shading\n" + " vec3 normal = normalize(cross( vec3(0.0, slope.y*heightScale, 2.0 / size.x), vec3(2.0 / size.y, slope.x*heightScale, 0.0)));\n" + " worldSpaceNormal = normal;\n" + "\n" + " // calculate position and transform to homogeneous clip space\n" + " vec4 pos = vec4(gl_Vertex.x, height * heightScale, gl_Vertex.z, 1.0);\n" + " gl_Position = gl_ModelViewProjectionMatrix * pos;\n" + "\n" + "eyeSpacePos = (gl_ModelViewMatrix * pos).xyz;\n" + "eyeSpaceNormal = (gl_NormalMatrix * normal).xyz;\n" + "}\n"; String fragmentShaderSource = "varying vec3 eyeSpacePos;\n" + "varying vec3 worldSpaceNormal;\n" + "varying vec3 eyeSpaceNormal;\n" + "" + "uniform vec4 deepColor;\n" + "uniform vec4 shallowColor;\n" + "uniform vec4 skyColor;\n" + "uniform vec3 lightDir;\n" + "\n" + "void main()\n" + "{" + " vec3 eyeVector = normalize(eyeSpacePos);\n" + " vec3 eyeSpaceNormalVector = normalize(eyeSpaceNormal);\n" + " vec3 worldSpaceNormalVector = normalize(worldSpaceNormal);\n" + " float facing = max(0.0, dot(eyeSpaceNormalVector, -eyeVector));\n" + " float fresnel = pow(1.0 - facing, 5.0); // Fresnel approximation\n" + " float diffuse = max(0.0, dot(worldSpaceNormalVector, lightDir)); \n" + " vec4 waterColor = mix(shallowColor, deepColor, facing);\n" + " gl_FragColor = waterColor*diffuse + skyColor*fresnel;\n" + "}\n"; int meshSize = 1024; int spectrumW = meshSize + 4; int spectrumH = meshSize + 1; // simulation parameters float g = 9.81f; // gravitational constant float A = 1e-7f; // wave scale factor float patchSize = 75.0f; // patch size float windSpeed = 100.0f; float windDir = (float) (Math.PI / 3.0f); 93 float dirDepend = 0.07f; } ssau.knyazev.oceanOcean.java package ssau.knyazev.ocean; import static jcuda.driver.CUgraphicsMapResourceFlags.CU_GRAPHICS_MAP_RESOURCE_FLAGS_WRITE_DISCARD; import static jcuda.driver.JCudaDriver.*; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Point; import java.awt.event.*; import java.io.*; import java.nio.*; import java.util.Random; import javax.media.opengl.*; import javax.media.opengl.awt.GLCanvas; import javax.media.opengl.glu.GLU; import javax.swing.JFrame; import javax.swing.SwingUtilities; import ssau.knyazev.fft.modules.cuda.CudaFFTModule; import ssau.knyazev.ocean.OceanConst; import jcuda.Pointer; import jcuda.Sizeof; import jcuda.driver.*; import jcuda.jcufft.JCufft; import jcuda.jcufft.cufftHandle; import jcuda.jcufft.cufftType; import com.jogamp.opengl.util.Animator; public class Ocean implements GLEventListener, OceanConst { private JFrame frame = null; private Animator animator = null; private GL2 gl = null; //OpenGL variables private int shaderProgramID = 0; private float translateX = 0.0f; private float translateY = 0.0f; private float translateZ = -2.0f; private float rotateX = 20f; private float rotateY = 0.0f; //OpenGL & Cuda variables private int posVertexBuffer = 0; private int heightVertexBuffer = 0; private int slopeVertexBuffer = 0; private int indexBuffer = 0; private CUgraphicsResource cuda_heightVB_resource = null; private CUgraphicsResource cuda_slopeVB_resource = null; // FFT data private cufftHandle fftPlan = null; private CUdeviceptr d_h0Ptr = null; private CUdeviceptr d_htPtr = null; private CUdeviceptr d_slopePtr = null; private float[] h_h0 = null; 94 //JCUDA private CUdevice device = null; private CUcontext glContext = null; private CUfunction generateSpectrumKernel = null; private CUfunction updateHeightmapKernel = null; private CUfunction calculateSlopeKernel = null; private CUmodule module = null; private float animTime = 0; private static long sum = 0; private static int cur = 1; private static int count = 50; public Ocean(GLCapabilities capabilities) { GLCanvas glComponent = new GLCanvas(capabilities); glComponent.setFocusable(true); glComponent.addGLEventListener(this); MouseControl mouseControl = new MouseControl(); glComponent.addMouseMotionListener(mouseControl); glComponent.addMouseWheelListener(mouseControl); frame = new JFrame("WaterSurface"); frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { animator.stop(); try{ cuMemFree(d_h0Ptr); cuMemFree(d_htPtr); cuMemFree(d_slopePtr); JCufft.cufftDestroy(fftPlan); gl.glDeleteBuffers(1, IntBuffer.wrap(new int[] {posVertexBuffer})); gl.glDeleteBuffers(1, IntBuffer.wrap(new int[] {heightVertexBuffer})); gl.glDeleteBuffers(1, IntBuffer.wrap(new int[] {slopeVertexBuffer})); } catch (Exception e1){ e1.printStackTrace(); } System.exit(0); } }); frame.setLayout(new BorderLayout()); glComponent.setPreferredSize(new Dimension(800, 800)); frame.add(glComponent, BorderLayout.CENTER); frame.pack(); frame.setVisible(true); glComponent.requestFocus(); // Create and start the animator animator = new Animator(glComponent); animator.start(); } public static void main(String args[]) { GLProfile profile = GLProfile.get(GLProfile.GL2); final GLCapabilities capabilities = new GLCapabilities(profile); SwingUtilities.invokeLater(new Runnable() { public void run() { new Ocean(capabilities); } }); } @Override 95 public void init(GLAutoDrawable drawable) { gl = drawable.getGL().getGL2(); gl.setSwapInterval(0); gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); gl.glEnable(GL2.GL_DEPTH_TEST); initShaders(gl); initJCuda(); initVBO(gl); } private void initJCuda(){ setExceptionsEnabled(true); cuInit(0); device = new CUdevice(); cuDeviceGet(device, 0); glContext = new CUcontext(); cuGLCtxCreate(glContext, CUctx_flags.CU_CTX_BLOCKING_SYNC, device); String ptxFileName = ""; try { ptxFileName = preparePtxFile("resources/ocean.cu"); } catch (IOException e) { System.err.println("Could not create PTX file"); throw new RuntimeException("Could not create PTX file", e); } module = new CUmodule(); cuModuleLoad(module, ptxFileName); generateSpectrumKernel = new CUfunction(); cuModuleGetFunction(generateSpectrumKernel, module, "generateSpectrumKernel"); updateHeightmapKernel = new CUfunction(); cuModuleGetFunction(updateHeightmapKernel, module, "updateHeightmapKernel"); calculateSlopeKernel = new CUfunction(); cuModuleGetFunction(calculateSlopeKernel, module, "calculateSlopeKernel"); fftPlan = new cufftHandle(); JCufft.cufftPlan2d(fftPlan, meshSize, meshSize, cufftType.CUFFT_C2C); int spectrumSize = spectrumW * spectrumH * 2; h_h0 = new float[spectrumSize]; h_h0 = generate_h0(h_h0); d_h0Ptr = new CUdeviceptr(); cuMemAlloc(d_h0Ptr, h_h0.length*Sizeof.FLOAT); cuMemcpyHtoD(d_h0Ptr, Pointer.to(h_h0), h_h0.length*Sizeof.FLOAT); int outputSize = meshSize*meshSize*Sizeof.FLOAT*2; d_htPtr = new CUdeviceptr(); d_slopePtr = new CUdeviceptr(); cuMemAlloc(d_htPtr, outputSize); cuMemAlloc(d_slopePtr, outputSize); } private void initVBO(GL2 gl) { int[] buffer = new int[1]; int size = meshSize*meshSize*Sizeof.FLOAT; gl.glGenBuffers(1, IntBuffer.wrap(buffer)); heightVertexBuffer = buffer[0]; gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, heightVertexBuffer); gl.glBufferData(GL2.GL_ARRAY_BUFFER, size, (Buffer) null, GL2.GL_DYNAMIC_DRAW); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0); cuda_heightVB_resource = new CUgraphicsResource(); cuGraphicsGLRegisterBuffer(cuda_heightVB_resource, heightVertexBuffer, CU_GRAPHICS_MAP_RESOURCE_FLAGS_WRITE_DISCARD); buffer = new int[1]; size = meshSize*meshSize*Sizeof.FLOAT*2; gl.glGenBuffers(1, IntBuffer.wrap(buffer)); slopeVertexBuffer = buffer[0]; 96 gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, slopeVertexBuffer); gl.glBufferData(GL2.GL_ARRAY_BUFFER, size, (Buffer) null, GL2.GL_DYNAMIC_DRAW); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0); cuda_slopeVB_resource = new CUgraphicsResource(); cuGraphicsGLRegisterBuffer(cuda_slopeVB_resource, slopeVertexBuffer, CU_GRAPHICS_MAP_RESOURCE_FLAGS_WRITE_DISCARD); buffer = new int[1]; size = meshSize*meshSize*Sizeof.FLOAT*4; gl.glGenBuffers(1, IntBuffer.wrap(buffer)); posVertexBuffer = buffer[0]; gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, posVertexBuffer); gl.glBufferData(GL2.GL_ARRAY_BUFFER, size, (Buffer) null, GL2.GL_DYNAMIC_DRAW); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, posVertexBuffer); ByteBuffer byteBuffer = gl.glMapBuffer(GL2.GL_ARRAY_BUFFER, GL2.GL_WRITE_ONLY); if (byteBuffer != null){ FloatBuffer put = byteBuffer.asFloatBuffer(); int index = 0; for (int y = 0; y < meshSize; y++) { for (int x = 0; x < meshSize; x++) { float u = x / (float) (meshSize - 1); float v = y / (float) (meshSize - 1); put.put(index, u * 2.0f - 1.0f); put.put(index+1, 0.0f); put.put(index + 2, v * 2.0f - 1.0f); put.put(index+3, 1.0f); index += 4; } } } gl.glUnmapBuffer(GL2.GL_ARRAY_BUFFER); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0); size = ((meshSize*2)+2)*(meshSize-1)*Sizeof.INT; buffer = new int[1]; gl.glGenBuffers(1, IntBuffer.wrap(buffer)); indexBuffer = buffer[0]; gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); gl.glBufferData(GL2.GL_ELEMENT_ARRAY_BUFFER, size, (Buffer) null, GL2.GL_STATIC_DRAW); byteBuffer = gl.glMapBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, GL2.GL_WRITE_ONLY); if (byteBuffer != null){ IntBuffer indices = byteBuffer.asIntBuffer(); int index = 0; for(int y=0; y<meshSize-1; y++) { for(int x=0; x<meshSize; x++) { indices.put(index, y*meshSize+x); indices.put(index+1, (y+1)*meshSize+x); index +=2; } indices.put(index, (y+1)*meshSize+(meshSize-1)); indices.put(index+1, (y+1)*meshSize); index += 2; } } gl.glUnmapBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER); gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, 0); } @Override public void display(GLAutoDrawable drawable) { float delta = System.nanoTime(); long time = System.nanoTime(); gl = drawable.getGL().getGL2(); runCuda(); gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT); 97 gl.glMatrixMode(GL2.GL_MODELVIEW); gl.glLoadIdentity(); gl.glTranslatef(translateX, translateY, translateZ); gl.glRotatef(rotateX, 1.0f, 0.0f, 0.0f); gl.glRotatef(rotateY, 0.0f, 1.0f, 0.0f); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, posVertexBuffer); gl.glVertexPointer(4, GL2.GL_FLOAT, 0, 0); gl.glEnableClientState(GL2.GL_VERTEX_ARRAY); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, heightVertexBuffer); gl.glClientActiveTexture(GL2.GL_TEXTURE0); gl.glTexCoordPointer(1, GL2.GL_FLOAT, 0, 0); gl.glEnableClientState(GL2.GL_TEXTURE_COORD_ARRAY); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, slopeVertexBuffer); gl.glClientActiveTexture(GL2.GL_TEXTURE1); gl.glTexCoordPointer(2, GL2.GL_FLOAT, 0, 0); gl.glEnableClientState(GL2.GL_TEXTURE_COORD_ARRAY); gl.glUseProgram(shaderProgramID); int uniHeightScale = gl.glGetUniformLocation(shaderProgramID, "heightScale"); gl.glUniform1f(uniHeightScale, 0.5f); int uniChopiness = gl.glGetUniformLocation(shaderProgramID, "chopiness"); gl.glUniform1f(uniChopiness, 1.0f); int uniSize = gl.glGetUniformLocation(shaderProgramID, "size"); gl.glUniform2f(uniSize, (float) meshSize, (float) meshSize); int uniDeepColor = gl.glGetUniformLocation(shaderProgramID, "deepColor"); gl.glUniform4f(uniDeepColor, 0.0f, 0.1f, 0.4f, 1.0f); int uniShallowColor = gl.glGetUniformLocation(shaderProgramID, "shallowColor"); gl.glUniform4f(uniShallowColor, 0.1f, 0.3f, 0.3f, 1.0f); int uniSkyColor = gl.glGetUniformLocation(shaderProgramID, "skyColor"); gl.glUniform4f(uniSkyColor, 1.0f, 1.0f, 1.0f, 1.0f); int uniLightDir = gl.glGetUniformLocation(shaderProgramID, "lightDir"); gl.glUniform3f(uniLightDir, 0.0f, 1.0f, 0.0f); gl.glColor3f(1.0f, 1.0f, 1.0f); gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); gl.glPolygonMode(GL2.GL_FRONT_AND_BACK, GL2.GL_FILL); gl.glDrawElements(GL2.GL_TRIANGLE_STRIP, ((meshSize * 2) + 2) * (meshSize - 1), GL2.GL_UNSIGNED_INT, 0); gl.glPolygonMode(GL2.GL_FRONT_AND_BACK, GL2.GL_FILL); gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, 0); gl.glDisableClientState(GL2.GL_VERTEX_ARRAY); gl.glClientActiveTexture(GL2.GL_TEXTURE0); gl.glDisableClientState(GL2.GL_TEXTURE_COORD_ARRAY); gl.glClientActiveTexture(GL2.GL_TEXTURE1); gl.glDisableClientState(GL2.GL_TEXTURE_COORD_ARRAY); gl.glUseProgram(0); drawable.swapBuffers(); time = System.nanoTime() - time; sum +=time; cur++; if (cur == count){ time = sum/count; System.out.println("DISPLAY TIME = " + time); } 98 delta = System.nanoTime() - delta; animTime += delta/1000000000; } @Override public void dispose(GLAutoDrawable drawable) { } @Override public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) { gl = drawable.getGL().getGL2(); GLU glu = GLU.createGLU(gl); gl.glViewport(0, 0, width, height); gl.glMatrixMode(GL2.GL_PROJECTION); gl.glLoadIdentity(); glu.gluPerspective(60.0, (double) width / (double) height, 0.1, 10.0); } private void runCuda() { Pointer kernelParameters = null; int blockX = 8; int blockY = 8; int gridX = meshSize/blockX; int gridY = gridX; kernelParameters = Pointer.to( Pointer.to(d_h0Ptr), Pointer.to(d_htPtr), Pointer.to(new int[] { spectrumW }), Pointer.to(new int[] { meshSize }), Pointer.to(new int[] { meshSize }), Pointer.to(new float[] { animTime }), Pointer.to(new float[] { patchSize })); cuLaunchKernel(generateSpectrumKernel, gridX, gridY, 1, // Grid dimension blockX, blockY, 1, // Block dimension 0, null, // Shared memory size and stream kernelParameters, null // Kernel- and extra parameters ); cuCtxSynchronize(); CudaFFTModule.createInstance().fft(d_htPtr, meshSize); CUdeviceptr g_hptr = new CUdeviceptr(); cuGraphicsMapResources(1, new CUgraphicsResource[]{cuda_heightVB_resource}, null); cuGraphicsResourceGetMappedPointer(g_hptr, new long[1], cuda_heightVB_resource); kernelParameters = Pointer.to( Pointer.to(g_hptr), Pointer.to(d_htPtr), Pointer.to(new int[] { meshSize })); cuLaunchKernel(updateHeightmapKernel, gridX, gridY, 1, // Grid dimension blockX, blockY, 1, // Block dimension 0, null, // Shared memory size and stream kernelParameters, null // Kernel- and extra parameters ); cuCtxSynchronize(); cuGraphicsUnmapResources(1, new CUgraphicsResource[]{cuda_heightVB_resource}, null); CUdeviceptr g_sptr = new CUdeviceptr(); cuGraphicsMapResources(1, new CUgraphicsResource[]{cuda_slopeVB_resource}, null); cuGraphicsResourceGetMappedPointer(g_sptr, new long[1], cuda_slopeVB_resource); kernelParameters = Pointer.to( Pointer.to(g_hptr), Pointer.to(g_sptr), 99 Pointer.to(new int[] { meshSize }), Pointer.to(new int[] { meshSize })); cuLaunchKernel(calculateSlopeKernel, gridX, gridY, 1, // Grid dimension blockX, blockY, 1, // Block dimension 0, null, // Shared memory size and stream kernelParameters, null // Kernel- and extra parameters ); cuCtxSynchronize(); cuGraphicsUnmapResources(1, new CUgraphicsResource[]{ cuda_slopeVB_resource}, null); } private void initShaders(GL2 gl) { shaderProgramID = gl.glCreateProgram(); attachShader(gl, GL2.GL_VERTEX_SHADER, vertexShaderSource); attachShader(gl, GL2.GL_FRAGMENT_SHADER, fragmentShaderSource); gl.glLinkProgram(shaderProgramID); int[] buffer = new int[1]; gl.glGetProgramiv(shaderProgramID, GL2.GL_LINK_STATUS, IntBuffer.wrap(buffer)); gl.glValidateProgram(shaderProgramID); } private int attachShader(GL2 gl, int type, String shaderSource){ int shader = 0; shader = gl.glCreateShader(type); gl.glShaderSource(shader, 1, new String[] { shaderSource }, null); gl.glCompileShader(shader); int[] buffer = new int[1]; gl.glGetShaderiv(shader, GL2.GL_COMPILE_STATUS, IntBuffer.wrap(buffer)); gl.glAttachShader(shaderProgramID, shader); gl.glDeleteShader(shader); return shader; } // Phillips spectrum // (Kx, Ky) - normalized wave vector // Vdir - wind angle in radians // V - wind speed // A - constant private float phillips(float Kx, float Ky, float Vdir, float V, float A, float dir_depend) { float k_squared = Kx*Kx + Ky*Ky; if (k_squared == 0.0f) return 0.0f; float L = V*V/g; float k_x = (float) (Kx / Math.sqrt(k_squared)); float k_y = (float) (Ky / Math.sqrt(k_squared)); float w_dot_k = (float) (k_x*Math.cos(Vdir) + k_y*Math.sin(Vdir)); float phillips = (float) (A*Math.exp(-1/(k_squared*L*L))/(k_squared * k_squared) * w_dot_k*w_dot_k); // filter out waves moving opposite to wind if (w_dot_k < 0.0f) phillips *= dir_depend; return phillips; } private float[] generate_h0(float[] h0) { Random rnd = new Random(); for (int y = 0; y <= meshSize; y++) { for (int x = 0; x <= meshSize; x++) { float kx = (float) ((-meshSize/2 + x) * (2*Math.PI / patchSize)); float ky = (float) ((-meshSize/2 + y) * (2*Math.PI / patchSize)); float P = (float) (Math.sqrt(phillips(kx, ky, windDir, windSpeed, A, dirDepend))); 100 if (kx == 0.0f && ky == 0.0f){ P = 0.0f; } float Er = (float) rnd.nextGaussian(); float Ei = (float) rnd.nextGaussian(); float h0_re = (float) (Er*P * Math.sqrt(2)); float h0_im = (float) (Ei*P * Math.sqrt(2)); int i = y * spectrumW + x; h0[2*i] = h0_re; h0[2*i + 1] = h0_im; } } return h0; } class MouseControl implements MouseMotionListener, MouseWheelListener { private Point previousMousePosition = new Point(); @Override public void mouseDragged(MouseEvent e) { int dx = e.getX() - previousMousePosition.x; int dy = e.getY() - previousMousePosition.y; if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK) { translateX += dx / 100.0f; translateY -= dy / 100.0f; } else if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == MouseEvent.BUTTON3_DOWN_MASK) { rotateX += dy; rotateY += dx; } previousMousePosition = e.getPoint(); } @Override public void mouseMoved(MouseEvent e) { previousMousePosition = e.getPoint(); } @Override public void mouseWheelMoved(MouseWheelEvent e) { translateZ += e.getWheelRotation() * 0.25f; previousMousePosition = e.getPoint(); } } private static String preparePtxFile(String cuFileName) throws IOException { int endIndex = cuFileName.lastIndexOf('.'); if (endIndex == -1) { endIndex = cuFileName.length() - 1; } String ptxFileName = cuFileName.substring(0, endIndex + 1) + "ptx"; File cuFile = new File(cuFileName); if (!cuFile.exists()) { throw new IOException("Input file not found: " + cuFileName); } String modelString = "-m" + System.getProperty("sun.arch.data.model"); String command = "nvcc " + modelString + " -ptx " + cuFile.getPath() + " -o " + ptxFileName; System.out.println("Executing\n" + command); Process process = Runtime.getRuntime().exec(command); String errorMessage = new String(toByteArray(process.getErrorStream())); String outputMessage = new String(toByteArray(process.getInputStream())); int exitValue = 0; 101 try { exitValue = process.waitFor(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Interrupted while waiting for nvcc output", e); } if (exitValue != 0) { System.out.println("nvcc process exitValue " + exitValue); System.out.println("errorMessage:\n" + errorMessage); System.out.println("outputMessage:\n" + outputMessage); throw new IOException("Could not create .ptx file: " + errorMessage); } System.out.println("Finished creating PTX file"); return ptxFileName; } private static byte[] toByteArray(InputStream inputStream) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte buffer[] = new byte[8192]; while (true) { int read = inputStream.read(buffer); if (read == -1) { break; } baos.write(buffer, 0, read); } return baos.toByteArray(); } } ssau.knyazev.fft.modules.cuda.CudaFFTModule.java package ssau.knyazev.fft.modules.cuda; import jcuda.Pointer; import jcuda.Sizeof; import jcuda.driver.CUcontext; import jcuda.driver.CUctx_flags; import jcuda.driver.CUdevice; import jcuda.driver.CUdeviceptr; import jcuda.driver.CUfunction; import jcuda.driver.CUmodule; import jcuda.driver.JCudaDriver; import ssau.knyazev.common.CudaCRuntimeException; import ssau.knyazev.fft.common.FFTConst; import ssau.knyazev.modules.CudaCompiler; import ssau.knyazev.modules.Generator; import ssau.knyazev.modules.Printer; public class CudaFFTModule { private static CudaFFTModule instance = null; private static long sum = 0; private static int count = 10; private CUdevice device = null; private CUcontext context = null; private CUfunction fftFunction = null; private CUfunction wlistFunction = null; private CUfunction invRowsFunction = null; private CUfunction invColsFunction = null; private CUfunction inverseFunction = null; private CUfunction fftRowsFunction = null; private CUfunction fftColsFunction = null; private int blockSize = 0; private int gridSize = 0; 102 private CudaFFTModule() throws CudaCRuntimeException{ JCudaDriver.setExceptionsEnabled(true); JCudaDriver.cuInit(0); device = new CUdevice(); JCudaDriver.cuDeviceGet(device, 0); context = new CUcontext(); JCudaDriver.cuCtxCreate(context, CUctx_flags.CU_CTX_BLOCKING_SYNC, device); String moduleName = CudaCompiler.preparePtxFile(FFTConst.CUDA_SIMPLE_MODULE_SRC, false); CUmodule module = new CUmodule(); JCudaDriver.cuModuleLoad(module, moduleName); fftFunction = new CUfunction(); JCudaDriver.cuModuleGetFunction(fftFunction, module, FFTConst.CUDA_FFT_FUNCTION_NAME); wlistFunction = new CUfunction(); JCudaDriver.cuModuleGetFunction(wlistFunction, module, FFTConst.CUDA_WLIST_FUNCTION_NAME); invColsFunction = new CUfunction(); JCudaDriver.cuModuleGetFunction(invColsFunction, module, "inverseCols"); invRowsFunction = new CUfunction(); JCudaDriver.cuModuleGetFunction(invRowsFunction, module, "inverseRows"); fftRowsFunction = new CUfunction(); JCudaDriver.cuModuleGetFunction(fftRowsFunction, module, "fftRows"); fftColsFunction = new CUfunction(); JCudaDriver.cuModuleGetFunction(fftColsFunction, module, "fftCols"); inverseFunction = new CUfunction(); JCudaDriver.cuModuleGetFunction(inverseFunction, module, "inverse"); } public static CudaFFTModule createInstance(){ if (instance != null){ return instance; } else { try { instance = new CudaFFTModule(); } catch (CudaCRuntimeException e) { e.printStackTrace(); } return instance; } } public void fft(CUdeviceptr srcPtr, int N){ blockSize = 16; gridSize = (N > blockSize) ? N/blockSize : 1; CUdeviceptr wlistPtr = new CUdeviceptr(); JCudaDriver.cuMemAlloc(wlistPtr, N*2 * Sizeof.FLOAT); //START FFT int size = 0; for (int x = N; x > 1; x = x >> 1){ size++; } inverse(N, size - 1, srcPtr); calculateWList(N, wlistPtr); for (int iteration = 0; iteration < size; iteration++){ Pointer kernelParameters = Pointer.to( Pointer.to(new int[] { N }), Pointer.to(srcPtr), Pointer.to(wlistPtr), Pointer.to(new int[] { iteration }) ); JCudaDriver.cuLaunchKernel(fftFunction, gridSize, gridSize, 1, blockSize, blockSize, 1, 0, null, kernelParameters, null ); JCudaDriver.cuCtxSynchronize(); } 103 //FINISH FFT JCudaDriver.cuMemFree(wlistPtr); } public float[][] fft(float[][] src) throws CudaCRuntimeException{ if (src != null && src.length == src[0].length){ src = transformToComplexMatrix(src); } blockSize = 16; gridSize = (src.length > blockSize) ? src.length/blockSize : 1; float[] arr = toVector(src); CUdeviceptr wlistPtr = new CUdeviceptr(); JCudaDriver.cuMemAlloc(wlistPtr, src[0].length * Sizeof.FLOAT); CUdeviceptr srcPtr = new CUdeviceptr(); JCudaDriver.cuMemAlloc(srcPtr, arr.length * Sizeof.FLOAT); JCudaDriver.cuMemcpyHtoD(srcPtr, Pointer.to(arr), arr.length * Sizeof.FLOAT); //START FFT long time = System.nanoTime(); int size = 0; for (int x = src.length; x > 1; x = x >> 1){ size++; } inverse(src.length, size - 1, srcPtr); calculateWList(src.length, wlistPtr); for (int iteration = 0; iteration < size; iteration++){ Pointer kernelParameters = Pointer.to( Pointer.to(new int[] { src.length }), Pointer.to(srcPtr), Pointer.to(wlistPtr), Pointer.to(new int[] { iteration }) ); JCudaDriver.cuLaunchKernel(fftFunction, gridSize, gridSize, 1, blockSize, blockSize, 1, 0, null, kernelParameters, null ); JCudaDriver.cuCtxSynchronize(); } time = System.nanoTime() - time; sum += time; //FINISH FFT JCudaDriver.cuMemcpyDtoH(Pointer.to(arr), srcPtr, arr.length * Sizeof.FLOAT); src = toMatrix(arr, src.length); //Printer.printVector(src); JCudaDriver.cuMemFree(wlistPtr); JCudaDriver.cuMemFree(srcPtr); return src; } public float[][] fftRowColMode(float[][] src) throws CudaCRuntimeException{ if (src != null && src.length == src[0].length){ src = transformToComplexMatrix(src); } blockSize = 16; gridSize = (src.length > blockSize) ? src.length/blockSize : 1; float[] arr = toVector(src); CUdeviceptr wlistPtr = new CUdeviceptr(); JCudaDriver.cuMemAlloc(wlistPtr, src[0].length * Sizeof.FLOAT); CUdeviceptr srcPtr = new CUdeviceptr(); JCudaDriver.cuMemAlloc(srcPtr, arr.length * Sizeof.FLOAT); JCudaDriver.cuMemcpyHtoD(srcPtr, Pointer.to(arr), arr.length * Sizeof.FLOAT); //START FFT long time = System.nanoTime(); 104 int size = 0; for (int x = src.length; x > 1; x = x >> 1){ size++; } inverse(src.length, size-1, srcPtr); calculateWList(src.length, wlistPtr); Pointer kernelParameters = Pointer.to( Pointer.to(new int[] { src.length }), Pointer.to(srcPtr), Pointer.to(wlistPtr) ); JCudaDriver.cuLaunchKernel(fftRowsFunction, gridSize, gridSize, 1, blockSize, blockSize, 1, 0, null, kernelParameters, null ); JCudaDriver.cuCtxSynchronize(); kernelParameters = Pointer.to( Pointer.to(new int[] { src.length }), Pointer.to(srcPtr), Pointer.to(wlistPtr) ); JCudaDriver.cuLaunchKernel(fftColsFunction, gridSize, gridSize, 1, blockSize, blockSize, 1, 0, null, kernelParameters, null ); JCudaDriver.cuCtxSynchronize(); time = System.nanoTime() - time; sum += time; //FINISH FFT JCudaDriver.cuMemcpyDtoH(Pointer.to(arr), srcPtr, arr.length * Sizeof.FLOAT); src = toMatrix(arr, src.length); JCudaDriver.cuMemFree(wlistPtr); JCudaDriver.cuMemFree(srcPtr); return src; } private void inverse(int length, CUdeviceptr srcPtr){ Pointer kernelParameters = Pointer.to( Pointer.to(new int[] { length}), Pointer.to(srcPtr) ); JCudaDriver.cuLaunchKernel(invRowsFunction, 8, 8, 1, 8, 8, 1, 0, null, kernelParameters, null ); JCudaDriver.cuCtxSynchronize(); kernelParameters = Pointer.to( Pointer.to(new int[] { length}), Pointer.to(srcPtr) ); JCudaDriver.cuLaunchKernel(invColsFunction, 8, 8, 1, 8, 8, 1, 0, null, kernelParameters, null ); JCudaDriver.cuCtxSynchronize(); } private void inverse(int length, int power, CUdeviceptr srcPtr){ 105 Pointer kernelParameters = Pointer.to( Pointer.to(new int[] { length}), Pointer.to(new int[] { power}), Pointer.to(srcPtr) ); JCudaDriver.cuLaunchKernel(inverseFunction, gridSize, gridSize, 1, blockSize, blockSize, 1, 0, null, kernelParameters, null ); JCudaDriver.cuCtxSynchronize(); } private void calculateWList(int length, CUdeviceptr wlistPtr){ Pointer kernelParameters = Pointer.to( Pointer.to(new int[] { length }), Pointer.to(wlistPtr) ); JCudaDriver.cuLaunchKernel(wlistFunction, gridSize, gridSize, 1, blockSize, blockSize, 1, 0, null, kernelParameters, null ); JCudaDriver.cuCtxSynchronize(); } protected static float[] toVector(float[][] src) { float[] res = new float[src.length * src[0].length]; int k = 0; for (int i = 0; i < src.length; i++) { for (int j = 0; j < src[0].length; j++) { res[k] = src[i][j]; k++; } } return res; } protected static float[][] toMatrix(float[] src, int rows) { float[][] res = new float[rows][src.length / rows]; int k = 0; for (int i = 0; i < rows; i++) { for (int j = 0; j < src.length / rows; j++) { res[i][j] = src[k]; k++; } } return res; } protected static float[][] transformToComplexMatrix(float[][] src){ float[][] res = new float[src.length][src[0].length*2]; for (int i = 0; i < src.length; i++){ for (int j = 0; j < src.length; j++){ res[i][j*2] = src[i][j]; } } return res; } public static void main(String[] args) { float[][] src = Generator.generateSimpleMatrix(2048); try { CudaFFTModule core = createInstance(); for (int i = 0; i < count; i++){ src = core.fft(src); } 106 } catch (CudaCRuntimeException e) { e.printStackTrace(); } long time = sum/count; System.out.println(time); } } resources/fft-c-module.cu __device__ float4 ccomplex_new() { return make_float4(0, 0, 0, 0); } __device__ float4 to_ccomplex(float2 arg) { return make_float4(arg.x, arg.y, 0, 0); } __device__ float2 to_complex(float4 arg) { return make_float2(arg.x - arg.w, arg.y + arg.z); } __device__ float4 ccomplex_expi(float arg) { return make_float4(cosf(arg), sinf(arg), 0, 0); } __device__ float4 ccomplex_expj(float arg) { return make_float4(cosf(arg), 0, sinf(arg), 0); } __device__ float2 conjugate(float2 arg) { return make_float2(arg.x, -arg.y); } __device__ float4 ccomplex_add(float4 a, float4 b) { return make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); } __device__ float4 ccomplex_sub(float4 a, float4 b){ return make_float4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); } __device__ int mirror_index(int srcIndex, int matrixWidth) { int matrixIndex = srcIndex/matrixWidth * matrixWidth; return (matrixIndex == srcIndex) ? srcIndex : matrixWidth - srcIndex + 2*matrixIndex; } __device__ void mirror(float4* src, int size, int down, int right, int dmirr, int rmirr) { int downright = size*down + right; 107 if (rmirr != right){ int downrmirr = down*size + rmirr; src[downrmirr].x = src[downright].x; src[downrmirr].y = src[downright].y; src[downrmirr].z = -src[downright].z; src[downrmirr].w = -src[downright].w; } if (dmirr != down){ int dmirrright = dmirr*size + right; src[dmirrright].x = src[downright].x; src[dmirrright].y = -src[downright].y; src[dmirrright].z = src[downright].z; src[dmirrright].w = -src[downright].w; } if (rmirr != right && dmirr != down){ int dmirrrmirr = dmirr*size + rmirr; src[dmirrrmirr].x = src[downright].x; src[dmirrrmirr].y = -src[downright].y; src[dmirrrmirr].z = -src[downright].z; src[dmirrrmirr].w = src[downright].w; } } __device__ float4 ccomplex_multy(float4 a, float4 b){ return make_float4( a.x * b.x - a.y * b.y - a.z * b.z + a.w * b.w, a.x * b.y + a.y * b.x - a.z * b.w - a.w * b.z, a.x * b.z - a.y * b.w + a.z * b.x - a.w * b.y, a.x * b.w + a.y * b.z + a.z * b.y + a.w * b.x); } __device__ __constant__ float _PI = 3.141592653; extern "C" __global__ void fft(int size, float4* a, float4* wilist, float4* wjlist, int iteration){ int idx = blockIdx.x * gridDim.x + threadIdx.x; int idy = blockIdx.y * gridDim.y + threadIdx.y; if (idx < size && idy < size){ int step = 1 << iteration; int step2 = step*2; bool flag = (idy/step) % 2 + (idx/step) % 2 > 0; if (!flag){ float4 w1 = wilist[(idy % step2)*(size/step2)]; float4 w2 = wjlist[(idx % step2)*(size/step2)]; int a00i = idy*size + idx; int a10i = (idy+step)*size + idx; int a01i = idy*size + (idx+step); int a11i = (idy+step)*size + (idx+step); float4 a00 = a[a00i]; float4 a10 = a[a10i]; float4 a01 = a[a01i]; float4 a11 = a[a11i]; float4 newa00 = ccomplex_new(); float4 newa01 = ccomplex_new(); float4 newa10 = ccomplex_new(); float4 newa11 = ccomplex_new(); newa00 = ccomplex_add(newa00, a00); newa10 = ccomplex_add(newa10, a00); newa01 = ccomplex_add(newa01, a00); newa11 = ccomplex_add(newa11, a00); float4 t = ccomplex_multy(w1, a10); newa00 = ccomplex_add(newa00, t); newa10 = ccomplex_sub(newa10, t); newa01 = ccomplex_add(newa01, t); newa11 = ccomplex_sub(newa11, t); 108 t = ccomplex_multy(w2, a01); newa00 = ccomplex_add(newa00, t); newa10 = ccomplex_add(newa10, t); newa01 = ccomplex_sub(newa01, t); newa11 = ccomplex_sub(newa11, t); t = ccomplex_multy(w2, a11); t = ccomplex_multy(w1, t); newa00 = ccomplex_add(newa00, t); newa10 = ccomplex_sub(newa10, t); newa01 = ccomplex_sub(newa01, t); newa11 = ccomplex_add(newa11, t); a[a00i] = newa00; a[a10i] = newa10; a[a01i] = newa01; a[a11i] = newa11; } } __syncthreads(); } extern "C" __global__ void fftMirror(int size, float4* a, float4* wilist, float4* wjlist, int iteration){ int idx = blockIdx.x * gridDim.x + threadIdx.x; int idy = blockIdx.y * gridDim.y + threadIdx.y; if (idx < size && idy < size){ int step = 1 << iteration; int matrixWidth = step << 1; int part = matrixWidth/4; int dmatrInd = idy/matrixWidth * matrixWidth; int rmatrInd = idx/matrixWidth * matrixWidth; int tdown = idy - dmatrInd; int tright = idx - rmatrInd; bool flag = (tdown <= part && tright <= part); if (flag){ float4 w1 = wilist[(idy % matrixWidth)*(size/matrixWidth)]; float4 w2 = wjlist[(idx % matrixWidth)*(size/matrixWidth)]; int a00i = idy*size + idx; int a10i = (idy+step)*size + idx; int a01i = idy*size + (idx+step); int a11i = (idy+step)*size + (idx+step); float4 a00 = a[a00i]; float4 a10 = a[a10i]; float4 a01 = a[a01i]; float4 a11 = a[a11i]; float4 newa00 = ccomplex_new(); float4 newa01 = ccomplex_new(); float4 newa10 = ccomplex_new(); float4 newa11 = ccomplex_new(); newa00 = ccomplex_add(newa00, a00); newa10 = ccomplex_add(newa10, a00); newa01 = ccomplex_add(newa01, a00); newa11 = ccomplex_add(newa11, a00); float4 t = ccomplex_multy(w1, a10); newa00 = ccomplex_add(newa00, t); newa10 = ccomplex_sub(newa10, t); newa01 = ccomplex_add(newa01, t); newa11 = ccomplex_sub(newa11, t); t = ccomplex_multy(w2, a01); newa00 = ccomplex_add(newa00, t); newa10 = ccomplex_add(newa10, t); newa01 = ccomplex_sub(newa01, t); 109 newa11 = ccomplex_sub(newa11, t); t = ccomplex_multy(w2, a11); t = ccomplex_multy(w1, t); newa00 = ccomplex_add(newa00, t); newa10 = ccomplex_sub(newa10, t); newa01 = ccomplex_sub(newa01, t); newa11 = ccomplex_add(newa11, t); a[a00i] = newa00; a[a10i] = newa10; a[a01i] = newa01; a[a11i] = newa11; if (iteration >= 2){ int rmirr = mirror_index(idx, matrixWidth); int dmirr = mirror_index(idy, matrixWidth); mirror(a, size, idy, idx, dmirr, rmirr); rmirr = mirror_index(idx + step, matrixWidth); mirror(a, size, idy, idx + step, dmirr, rmirr); dmirr = mirror_index(idy + step, matrixWidth); rmirr = mirror_index(idx, matrixWidth); mirror(a, size, idy + step, idx, dmirr, rmirr); rmirr = mirror_index(idx + step, matrixWidth); mirror(a, size, idy + step, idx + step, dmirr, rmirr); } } } __syncthreads(); } extern "C" __global__ void wlist(int size, float4* wi, float4* wj){ int blockSize = blockDim.x * blockDim.y; int block = blockIdx.y * gridDim.x + blockIdx.x; int idx = (blockSize * block) + threadIdx.y * blockDim.x + threadIdx.x; if (idx < size){ wi[idx] = ccomplex_expi(-2*_PI*idx/size); wj[idx] = ccomplex_expj(-2*_PI*idx/size); } } extern "C" __global__ void inverseRows(int size, float4* src){ int blockSize = blockDim.x * blockDim.y; int block = blockIdx.y * gridDim.x + blockIdx.x; int idx = (blockSize * block) + threadIdx.y * blockDim.x + threadIdx.x; int col = idx; if (idx < size){ int icol = 0; int jcol = 0; int dl = size/2; int st = size - 1; int j = 0; for (int i = 0; i < st; i++){ if (i < j){ icol = i*size + col; jcol = j*size + col; float4 s0 = src[icol]; src[icol] = src[jcol]; src[jcol] = s0; } int k = dl; while (k <= j){ j = j - k; 110 k = k >> 1; } j = j + k; } } } extern "C" __global__ void inverseCols(int size, float4* src){ int blockSize = blockDim.x * blockDim.y; int block = blockIdx.y * gridDim.x + blockIdx.x; int idx = (blockSize * block) + threadIdx.y * blockDim.x + threadIdx.x; int row = idx; if (idx < size){ int rowi = 0; int rowj = 0; int dl = size/2; int st = size - 1; int j = 0; for (int i = 0; i < st; i++){ if (i < j){ rowi = row*size + i; rowj = row*size + j; float4 s0 = src[rowi]; src[rowi] = src[rowj]; src[rowj] = s0; } int k = dl; while (k <= j){ j = j - k; k = k >> 1; } j = j + k; } } } extern "C" __global__ void inverse(int size, int power, float4* src){ int idx = blockIdx.x * gridDim.x + threadIdx.x; int idy = blockIdx.y * gridDim.y + threadIdx.y; if (idx < size && idy < size){ int indexy = idy; int indexx = idx; int newrow = 0; int newcol = 0; for (int l = power; l >= 0; l--){ int k = 1 << l; if (k <= indexy){ indexy -= k; int delta = 1 << (power-l); newrow += delta; } if (k <= indexx){ indexx -= k; int delta = 1 << (power-l); newcol += delta; } } int rowcol = idy*size + idx; int newrownewcol = newrow*size + newcol; if (idy > newrow){ float4 t = src[rowcol]; src[rowcol] = src[newrownewcol]; src[newrownewcol] = t; } else if (idy == newrow && idx > newcol){ float4 t = src[rowcol]; src[rowcol] = src[newrownewcol]; 111 src[newrownewcol] = t; } } } extern "C" __global__ void convertToCComplex(int size, float2* src, float4* out){ int idx = blockIdx.x * gridDim.x + threadIdx.x; int idy = blockIdx.y * gridDim.y + threadIdx.y; if (idx < size && idy < size){ int uid = idy*size + idx; out[uid] = to_ccomplex(src[uid]); } } extern "C" __global__ void convertToComplex(int size, float2* src, float4* out){ int idx = blockIdx.x * gridDim.x + threadIdx.x; int idy = blockIdx.y * gridDim.y + threadIdx.y; if (idx < size && idy < size){ int uid = idy*size + idx; src[uid] = to_complex(out[uid]); } } resources/ocean.cu /////////////////////////////////////////////////////////////////////////////// #include <cufft.h> #include <math_constants.h> //Round a / b to nearest higher integer value int cuda_iDivUp(int a, int b) { return (a + (b - 1)) / b; } // complex math functions __device__ float2 conjugate(float2 arg) { return make_float2(arg.x, -arg.y); } __device__ float2 complex_exp(float arg) { return make_float2(cosf(arg), sinf(arg)); } __device__ float2 complex_add(float2 a, float2 b) { return make_float2(a.x + b.x, a.y + b.y); } __device__ float2 complex_mult(float2 ab, float2 cd) { return make_float2(ab.x * cd.x - ab.y * cd.y, ab.x * cd.y + ab.y * cd.x); } // generate wave heightfield at time t based on initial heightfield and dispersion relationship extern "C" __global__ void generateSpectrumKernel(float2* h0, float2 *ht, unsigned int in_width, unsigned int out_width, unsigned int out_height, 112 float t, float patchSize) { unsigned int x = blockIdx.x*blockDim.x + threadIdx.x; unsigned int y = blockIdx.y*blockDim.y + threadIdx.y; unsigned int in_index = y*in_width+x; unsigned int in_mindex = (out_height - y)*in_width + (out_width - x); // mirrored unsigned int out_index = y*out_width+x; // calculate wave vector float2 k; k.x = (-(int)out_width / 2.0f + x) * (2.0f * CUDART_PI_F / patchSize); k.y = (-(int)out_width / 2.0f + y) * (2.0f * CUDART_PI_F / patchSize); // calculate dispersion w(k) float k_len = sqrtf(k.x*k.x + k.y*k.y); float w = sqrtf(9.81f * k_len); if ((x < out_width) && (y < out_height)) { float2 h0_k = h0[in_index]; float2 h0_mk = h0[in_mindex]; // output frequency-space complex values ht[out_index] = complex_add( complex_mult(h0_k, complex_exp(w * t)), complex_mult(conjugate(h0_mk), complex_exp(-w * t)) ); //ht[out_index] = h0_k; } } // update height map values based on output of FFT extern "C" __global__ void updateHeightmapKernel(float* heightMap, float2* ht, unsigned int width) { unsigned int x = blockIdx.x*blockDim.x + threadIdx.x; unsigned int y = blockIdx.y*blockDim.y + threadIdx.y; unsigned int i = y*width+x; // cos(pi * (m1 + m2)) float sign_correction = ((x + y) & 0x01) ? -1.0f : 1.0f; heightMap[i] = ht[i].x * sign_correction; } // generate slope by partial differences in spatial domain extern "C" __global__ void calculateSlopeKernel(float* h, float2 *slopeOut, unsigned int width, unsigned int height) { unsigned int x = blockIdx.x*blockDim.x + threadIdx.x; unsigned int y = blockIdx.y*blockDim.y + threadIdx.y; unsigned int i = y*width+x; float2 slope = make_float2(0.0f, 0.0f); if ((x > 0) && (y > 0) && (x < width-1) && (y < height-1)) { slope.x = h[i+1] - h[i-1]; slope.y = h[i+width] - h[i-width]; } slopeOut[i] = slope; } 113