Структура программы для центрального процессора
Приложение, работающее с графическим процессором, состоит из нескольких логических блоков, большинство из которых исполняется на центральном процессоре. Структура программы для центрального процессора показана на рис. 2.8.
В настоящем примере программа для центрального процессора написана на языке С#, она приведена ниже. Поскольку подробное рассмотрение синтаксиса C# выходит за рамки настоящего пособия, детально прокомментированы только операции, непосредственно касающиеся графического процессора.
В качестве интерфейса (API), обеспечивающего взаимодействие пользовательского приложения с графическим процессором, мы используем библиотеку Microsoft DirectX с дополнительной библиотекой Microsoft XNA Game Studio Express 2.0, упрощающей программирование.

Рис. 2.8. Последовательность операций, выполняемых центральным процессором при взаимодействии с GPU
Реализация программы для центрального процессора на С#
Ниже приведен полный текст программы для CPU, организующей взаимодействие с графическим процессором. Программа написана на языке C# из состава Microsoft Visual Studio 2005. Используется библиотека Microsoft XNA Game Studio Express 2.0. Комментарии, вставленные в текст без использования символов “//” или не соответствуют синтаксису С#, но если их удалить из текста, то программа будет работать.
Подключение необходимых библиотек
Программой используются стандартная библиотека System, а также библиотека Microsoft.Xna. Framework, которая реализует среду XN А Game Studio и осуществляет взаимодействие с DirectX. Раздел этой библиотеки Microsoft.Xna. Framework.Graphics подключаем отдельно, чтобы сократить в тексте программы описание доступа к ресурсам этого раздела.
using System;
using Microsoft.Xna. Framework;
using Microsoft.Xna.Framework.Graphics;
public class GPGPU: Microsoft.Xna.Framework.Game
{
//Описание переменных, используемых программой /* Задается размер матрицы, количество элементов в строке/столб- це */
int size = 3;
/* Описываем переменную graphics класса GraphicsDeviceManager и затем инициализируем эту переменную операцией graphics = new GraphicsDeviceManager (this). Эта переменная будет обеспечивать доступ к процедурам для управления графическим процессором из библиотеки Microsoft.Xna. Framework */
GraphicsDeviceManager graphics;
//Стандартные процедуры, реализующие исполнение программы на C#
/* Ниже описаны процедуры, которые будут последовательно исполнены сразу после запуска программы. Процедуры Main и Initialize являются стандартными для проектов на С#. Внутри процедуры Initialize находится вызов процедуры MatrixSum (), которая и осуществляет суммирование матриц на графическом процессоре. */
static void Main (string [] args) {using (GPGPU game = new GPGPU ()) game.Run ();}
public GPGPU () {graphics = new GraphicsDeviceManager (this);} protected override void Initialize ()
{
base. Initialize ();
MatrixSum ();
Exit ();
}
//Процедура, суммирующая матрицы с использованием GPU private void MatrixSum ()
{
/* Выше была введена переменная graphics класса GraphicsDeviceManager. В этом классе есть нужный нам подкласс GraphicsDevice. Создаем переменную gpu этого подкласса */
GraphicsDevice gpu = graphics.GraphicsDevice;
//Выделение памяти для рендер-цели
/* Созданная в начале программы переменная size = 3 задает размеры матриц (3x3), которые мы будем складывать. Теперь создается рендер-цель — массив 3 х 3, в который будет выводиться результат расчетов. Соответствующая переменная называется tex_target. Она является не простым массивом 3 х 3, а принадлежит к классу RenderTarget2D из библиотеки Microsoft.Xna.Framework. Кроме самого массива в объектах этого класса хранятся некоторые дополнительные параметры, в частности, указатель на переменную gpu, который задает конкретную видеокарту, для которой создается рендер-цель. */
RenderTarget2D tex_target = new RenderTarget2D (gpu, size, size, 1, SurfaceFormat. Single);
//Выделение памяти для текстур с исходными данными /* Создаются текстуры — массивы размерами size*size, — в которых будут храниться исходные матрицы. Как и рендер-цель, соответствующие переменные не являются простыми массивами, а принадлежат к более сложному классу Texture2D, в объектах которого, кроме самих массивов, хранятся дополнительные параметры. Как и выше, при создании новой переменной класса, значения этих параметров устанавливаются строкой newTexture2D (gpu, size, size, 1, Resource Usage. Dynamic, SurfaceFormat.Single, ResourceManagementMode.Manual), значениями внутри скобок. */
Texture2D tex_matrixl = newTexture2D (gpu, size, size, 1, ResourceUsage. Dynamic, SurfaceFormat.Single, ResourceManagementMode.Manual);
Texture2D texmatrix2 = newTexture2D (gpu, size, size, 1, ResourceUsage. Dynamic, SurfaceFormat.Single, ResourceManagementMode.Manual);
//Заполнение массивов исходных данных конкретными значениями
/* Теперь задаем конкретные значения элементов исходных матриц. Сначала эти значения записываются в одномерные массивы matrix 1 и matrix2 размерами size * size. Элементы а9 и b9 после преобразования матриц к одномерным массивам будут иметь номера j * size + i. Затем мы копируем созданные массивы в текстуры класса Texture2D tex_matrixl и tex_matrix2 с помощью операции tex matrixl. SetData
}
tex_matrixl.SetData
/*Следующая задача — установление соответствия между целочисленными индексами элементов матриц 3x3 и текстурными координатами этих же элементов внутри квадрата (-1;-1)- (1; 1), или quad’a, которыми будет оперировать графический процессор, а также внутри квадрата (0;0)-(1;1), в который отображается массив результатов (рендер-цель). Для этого достаточно задать соответствие между индексами «крайних» элементов матриц и координатами вершин квадрата. Соответствие это показано ниже. При необходимости рассмотрения матриц других размеров, знаменатель 6 нужно заменить на значение 2*size.
Соответствие между координатами вершин quad’a (-1;-1)-(1;1)и координатами элементов складываемых матриц:
- (-1; 1)..............(1/6; 1/6)
- (—1; —1)............(1/6; 1+1/6)
- (1; 1).................(1+1/6; 1/6)
- (1; —1)..............(1+1/6; 1+1/6)
Метод определения координат элементов проиллюстрирован также на рис. 2.4. Видно, что приведенные формулы действительно обеспечивают попадание элементов массива 3 х 3 в необходимые области. Учтен также тот факт, что ось Y на экране (и в рендер-цели) направлена сверху вниз.
Третья пространственная координата во входных данных у нас не используется, поэтому в конструкторах вида Vector3 (-1, 1, 0) третья координата равна нулю, тогда как первые два значения задают координаты вершин quad’a */
float dx = 0.5f/size, dy = 0.5f/size;//cMemeHHH для адресации центров текселей
VertexPositionTexture [] v = new VertexPositionTexture [] { new VertexPositionTexture (new Vector3 (—1, 1, 0), new Vector2 (0 + dx, 0 + dy)),
new VertexPositionTexture (new Vector3 (—1,-1, 0), new Vector2 (0 + dx, 1 + dy)), new VertexPositionTexture (new Vector3 (1,1,0), new Vector2 (1 + dx, 0 + dy»,
new VertexPositionTexture (new Vector3 (1,-1, 0), new Vector2 (1 + dx, 1 + dy))
}
/* Следующей стандартной строкой выделяется область памяти для хранения координат вешин quad’a. Этих вершин 4, чем и обусловлен размер 4 * VertexPositionTexture.SizelnBytes */
VertexBuffer quad = new VertexBuffer (gpu, 4 * VertexPositionTexture. SizelnBytes, ResourceUsage.None, ResourceManagementMode.Automatic); quad.SetData
VertexDeclaration quadVertexDecIaration = new VertexDecIaration (gpu, VertexPositionTexture.VertexElements);
//Компилирование эффектов, содержащих шейдеры /* В библиотеку Microsoft.Xna. Framework встроен компилятор текстовых файлов, содержащих шейдеры для графического процессора, таких как sum.fx (см. начало примера и прил. 1). Следующая строка компилирует файл sum.fx и записывает полученную программу (в машинных кодах) в область оперативной памяти, на которую указывает переменная е типа CompiledEffect. */
Compiled Effect е = Effect.CompileEffectFromFile (“sum.fx”, null, null, CompilerOptions.None, TargetPlatform.Windows);
/* Проверка успешности компиляции */
if (! e.Success) throw new Exception (e.ErrorsAndWarnings);
//Выбор шейдера, который будет исполняться /* Создается новая переменная, описывающая эффект, который будет исполняться на графическом процессоре. В качестве кода эффекта выбирается откомпилированная программа, на которую указывает переменная е. */
Effect fx = new Effect (gpu, e.GetEffectCode (), CompilerOptions.None, null);
/* В качестве запускаемой на видеокарте процедуры выбирается техника sum из файла sum.fx (описана выше) */ fx.CurrentTechnique = fx.Techniques [“sum”];
/* Копирование исходных данных в видеопамять, доступную графическому процессору. В качестве исходных данных передаем эффекту fix, текстуры с исходными данными tex matrixl и tex_matrix2, которые мы уже создали в начале программы: */
fx.Parameters [“tex_matrixl”].SetValue (tex matrixl); fx.Parameters [“tex_matrix2”].SetValue (tex_matrix2);
/* Задаем выходные параметры: quad и рендер-цель: */ gpu.Vertices [0].SetSource (quad, 0, VertexPositionTexture.SizelnBytes); gpu.VertexDeclaration = quadVertexDeclaration; gpu.SetRenderTarget (0, tex_target);
//Исполнение шейдера на графическом процессоре fx.Begin 0;
fx.CurrentTechnique.Passes [“sum”].Begin (); gpu.DrawPrimitives (PrimitiveType.TriangleStrip, 0, 2); fx.CurrentTechnique.Passes [“sum”].End (); fx.End 0;
/* Получение результата из рендер-цели и сохранение его в текстовом файле
Копирование массива результатов из видеопамяти в оперативную память, доступную центральному процессору */ gpu.ResolveRenderTarget (0);
/* Создаем текстовый файл для записи результата */ using (System.IO. StreamWriter w = new System.IO. StreamWriter (“results.txt”))
{
/* Создаем одномерный массив, в который будут скопированы данные из рендер-цели, для дальнейшей обработки центральным процессором */ float [] matrix3 = new float [size * size];
/* Копирование данных из рендер-цели в массив matrix3. Аргумент
tex_target.GetTexture ().GetData
/* Запись данных из массива matrix3 в текстовый файл */ for (int j = 0; j < size; j++)
{for (int i = 0; i < size; i++) w.Write (“ {0}”, matrix3 [j * size + i]); w.WriteLine ();}}}}
Операции, вошедшие в рассмотренный пример, составляют только небольшую часть всех возможностей библиотеки XNA, большинство функций которой предназначены для отображения графики в играх. Продемонстрированных операций достаточно для того, чтобы инициализировать физико-математические расчеты на GPU и получить результаты из рендер-цели.