Running Simulations on the Graphics Processing Unit (GPU)
CS 493/693 Lecture, Dr. Lawlor
This is all well and good for drawing pictures, but there are lots of
other problems out there that don't involve pictures in any way.
Or do they? All computation is just data manipulation, and we can
write *anything* in a pixel shader--floats are floats, after all!
Deep down, the GPU supports a fairly small number of primitives:
- Pixel programs, which we write in GLSL just like writing shaders for graphics use. You can
compile a whole set of different pixel programs, and then switch
between them pretty quickly.
- Textures, which are 2D rectangular arrays of pixels. That
sounds graphics-specific, but you can think of this as "2D array of
floats". Again, you can have several textures in one computation.
- Framebuffer objects, which let you run pixel programs that read from arbitrary textures and write to one texture.
The only annoying part is that though a pixel program can read from any location on any texture it likes, it can only write
to its own pixel. And no, you can't bind the same texture for
both reads and writes (at least not reliably). Hey, that's because RAR is not a
dependency, but WAR/RAW/WAW is!
I wrote a little wrapper for GLSL. The main class is
"gpu_array",
which is a 2D grid of pixels. All the OpenGL side stuff is hidden
inside gpu_env--just make one, and pass it to each gpu_array.
Creating gpu_arrays is expensive (texture and framebuffer allocation is
ridiculously slow), but you can operate on or swap the arrays cheaply.
Here are some examples:
#include <iostream>
#include "ogl/gpgpu.h"
#include "ogl/glsl.cpp"
#include "ogl/glew.c" /* include entire body here, for easy linking */
int main() {
gpu_env e; /* make the environment (OpenGL window &c) */
const int n=16;
gpu_array A(e,"A",n,1,0,GL_LUMINANCE32F_ARB);
GPU_RUN(A,
gl_FragColor=vec4(location.x);
)
float out[n];
A.read(out,n,1);
for (int i=0;i<n;i++)
std::cout<<"out["<<i<<"]="<<out[i]<<" = "<<out[i]*n<<"/"<<n<<"\n";
return 0;
}
(Try this in NetRun now!)
Here we have two textures, and B reads from A.
/*
Tiny demo GLSL program, used for GPGPU computing.
Dr. Orion Lawlor, olawlor@acm.org, 2009-04-17 (Public Domain)
*/
#include "ogl/glew.c"
#include "ogl/gpgpu.h"
#include "ogl/glsl.cpp"
#include <iostream>
#include <math.h>
int main() {
int w=4, h=1;
const int n=w*h;
// prepare CPU-side arrays
float *a=new float[n], *b=new float[n];
for (int i=0;i<n;i++) {
a[i]=rand()%4;
}
// prepare GPU-side arrays (and copy data over)
gpu_env e;
gpu_array A(e,"A",w,h,a,GL_LUMINANCE32F_ARB);
gpu_array B(e,"B",w,h,b,GL_LUMINANCE32F_ARB);
// compute stuff on the GPU
GPU_RUN(B,
gl_FragColor=texture2D(A,location) + 1.0;
)
// copy back to the CPU
B.read(b,w,h);
for (int i=0;i<n;i++) {
std::cout<<"a["<<i<<"]= "<<a[i]<<" and b["<<i<<"] = "<<b[i]<<"\n";
}
return 0;
}
(Try this in NetRun now!)
The "gpucloth" example program is a complete example of this method,
including shaders for the position update, velocity update (including
net force calculation), and rendering.