Этапы алгоритма моделирования, исполняемые на CPU
Программа для графического процессора должна запускаться внутри полного алгоритма молекулярной динамики, остальные части которого исполняются на центральном процессоре. Ниже мы рассмотрим реализацию этого общего алгоритма на языке C# с использованием библиотеки процедур XNA2. Для проведения расчёта на графическом процессоре во внешней, вызывающей этот расчёт, программе, необходимо выполнить ряд операций: · Создать объект (в DirectX – объект типа GraphicsDeviceManager), реализующий доступ к графическому процессору; Рис. 6.6. Операции, исполняемые центральным процессором · Выделить память (т.е., описать переменные) для массивов, в которых будут храниться входные данные, – координаты частиц и параметры потенциалов взаимодействия, – а также выходные данные – компоненты векторов сил, действующих на ионы; · Представить входные данные в форме, требуемой для их передачи графическому процессору; · Выделить память для хранения координат вершин квада (описание понятия «квад» приведено выше) и задать значения этих координат; · Обратиться к текстовому файлу, содержащему код шейдера (программы для графического процессора). Откомпилировать этот код и записать полученную программу в память, доступную графическому процессору (перечисленные задачи программируются несложно, поскольку в библиотеке XNA2 для этого есть стандартные процедуры); · Передать графическому процессору исходные данные; · Исполнить необходимые расчёты; · Скопировать результат из рендер-цели для дальнейшего использования в программе на центральном процессоре; Операции, которые необходимо выполнить для организации расчёта, обобщены в блок-схеме на рис. 6.6. 6.3.2. Процедуры на C#, обеспечивающие работу с графическим процессором Ниже приведён текст программы на C#, в котором выделены и прокомментированы все перечисленные этапы. В этом тексте присутствуют и все необходимые вспомогательные операции, также с комментариями. private void InitGPU() /* Подготовительные операции для расчёта на графическом процессоре */ { //Локальная переменная, представляющая графический процессор gpu = graphics.GraphicsDevice;
//Создаем текстуру размером w * h, в которой будут храниться координаты и типы частиц. Количество ячеек в этой текстуре (w*h) должно совпадать с количеством частиц в кристалле, либо превышать это количество (в последнем случае «лишние» ячейки не используются). Расчёт на GPU будет наиболее быстрым, если w и h кратны 32. in_pos = new Texture2D(gpu, w, h, 1, TextureUsage.None, SurfaceFormat.Vector4);
//Создаём рендер-цель – текстуру размером w * h, в которую попадет результат расчета сил – в её ячейках будут храниться силы, действующие на каждый из ионов. out_force = new RenderTarget2D(gpu, w, h, 1, SurfaceFormat.Vector4, RenderTargetUsage.PreserveContents);
// Задаем текстуру размером 2 * 2, с параметрами парного межчастичного потенциала in_coefs = new Texture2D(gpu, 2, 2, 1, TextureUsage.None, SurfaceFormat.Vector4); float[] data = new float[2 * 2 * 4]; for (int u = 0; u < 2; u++) for (int v = 0; v < 2; v++) //здесь 2 цикла могли быть заменены одним по i от 0 //до < количества пар> - 1 { int i = (u * 2 + v) * 4; data[i] = (float)coefs[i++]; data[i] = (float)coefs[i++]; data[i] = (float)coefs[i++]; data[i] = (float)coefs[i++]; } in_coefs.SetData< float> (data); //in_coefs - переменная типа Texture2D
//Задаем квад (вершинные и текстурные координаты, соответственно). //Y-координата текстуры инвертирована по отношению к Y-координате вершины, //т.к. ось Y направлена снизу вверх в пространстве вершин и сверху вниз в пространстве экрана.
float du = 0.5f / w, dv = 0.5f / h; // смещения для адресации текселей по центрам //(в случае текстур с размерами не равными степени числа 2 quad = new VertexBuffer(gpu, typeof(VertexPositionTexture), 4, BufferUsage.None); //переменная типа VertexBuffer quad.SetData< VertexPositionTexture> (new VertexPositionTexture[] { new VertexPositionTexture(new Vector3(-1, 1, 0), new Vector2(0 + du, 0 + dv)), new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1 + du, 0 + dv)), new VertexPositionTexture(new Vector3(-1, -1, 0), new Vector2(0 + du, 1 + dv)), new VertexPositionTexture(new Vector3(1, -1, 0), new Vector2(1 + du, 1 + dv)) }); // кубу в пространстве сопоставлены смещенные углы плоскости gpu.Vertices[0].SetSource(quad, 0, VertexPositionTexture.SizeInBytes); //размер квада gpu.VertexDeclaration = new VertexDeclaration(gpu, VertexPositionTexture.VertexElements); //неясно, что
//Задаем шейдер и его входные параметры. //Компиляция текстового файла с шейдером CompiledEffect e = Effect.CompileEffectFromFile(" force.fx", null, null, CompilerOptions.None, TargetPlatform.Windows); //Проверка успешности компиляции if (! e.Success) throw new Exception(e.ErrorsAndWarnings); //Записывает программу эффекта в память видеокарты fx = new Effect(gpu, e.GetEffectCode(), CompilerOptions.None, null); //переменная типа Effect //Выбирает роцедуру из кода эффекта, которую надо будет выполнять fx.CurrentTechnique = fx.Techniques[" Force" ]; //Передача на видеокарту координат частиц fx.Parameters[" in_pos" ].SetValue(in_pos); //видимо, in_pos и in_coefs описаны в шейдере как входные текстуры //Передача на видеокарту параметров потенциалов взаимодействия fx.Parameters[" in_coefs" ].SetValue(in_coefs); } private void ForceGPU() { int i, u, v;
//Задаем текстуру с координатами и типами частиц float[] data = new float[w * h * 4];
/* Текстура представляет собой таблицу с размером w по горизонтали и h по вертикали. * В каждой ячейке таблицы находится 4-х-вектор, хранящий координаты и тип ионов. * Номер частицы в линейном массиве i вычисляется по формуле i = v * w + u */ for (v = 0; v < h; v++) for (u = 0; u < w; u++) { i = v * w + u; data[i * 4 + 0] = (float)pos[i * 3 + 0]; data[i * 4 + 1] = (float)pos[i * 3 + 1]; data[i * 4 + 2] = (float)pos[i * 3 + 2]; data[i * 4 + 3] = type[i] / 2f; } in_pos.SetData< float> (data); //in_pos - переменная типа Texture2D
//Задаем новую рендер-цель, сохраняя старую. RenderTarget2D old_render_target = (RenderTarget2D)gpu.GetRenderTarget(0); //Зачем нужно преобразование типа? gpu.SetRenderTarget(0, out_force); //out_force - переменная типа RenderTarget2D gpu.Clear(new Color(0, 0, 0, 0)); //Обнуление сил в рендер-цели
fx.Begin(); //спросить, что делает Begin. Загружает в память? for (i = 0; i < ions; i++) { //Обновляем ссылку на рендер-цель (необходимо для корректного выполнения операции += в шейдере) gpu.SetRenderTarget(0, null); fx.Parameters[" out_force" ].SetValue(out_force.GetTexture()); gpu.SetRenderTarget(0, out_force); //Передаем в шейдер текстурные координаты i-й частицы. u = i % w; v = i / w; //очевидно, % - остаток от деления fx.Parameters[" u_i" ].SetValue((u + 0.5f) / w); //передаётся центр ячейки fx.Parameters[" v_i" ].SetValue((v + 0.5f) / h); //передаётся центр ячейки
//Вычисляем взаимодействие i-й частицы со всеми остальными. fx.CurrentTechnique.Passes[" force_i" ].Begin(); gpu.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2); fx.CurrentTechnique.Passes[" force_i" ].End(); } fx.End(); //В цикле по всем частицам заполняется одна и та же рендер-цель
//Восстанавливаем старую рендер-цель. gpu.SetRenderTarget(0, old_render_target);
//Получаем результат из новой рендер-цели data = new float[w * h * 4]; for (i = 0; i < 3; i++) gpu.Textures[i] = null; out_force.GetTexture().GetData< float> (data); //out_force - массив с рендер-целью for (v = 0; v < h; v++) for (u = 0; u < w; u++) { i = v * w + u; force[i * 3 + 0] = data[i * 4 + 0]; force[i * 3 + 1] = data[i * 4 + 1]; force[i * 3 + 2] = data[i * 4 + 2]; } }
|