processing
GPGPU
aparapi

GPUを利用して高速演算を実行する

More than 1 year has passed since last update.

(Processing Advent Calendar 2016 25日目)

概要

ProcessingでGPUを利用した高速演算を行う。

背景

Processingを利用してスケッチを作ったものの、あまりにも速度が遅くて実用に耐えられなかったときありませんか。
Processingの標準機能を利用するのであれば、GLSLを利用してShaderを書くことで描画演算を高速に行うことができると思います。
しかし、Shader言語は難しくすでに作ってしまったスケッチを移植するのは難易度が高すぎるわけで。

そこでGPUの並列演算を利用しつつも、Processingらしさをなるべく崩さずにできないかとおもい、
JavaからGPGPU(OpenCL)を呼び出すライブラリであるaparapiを利用することにしました。

参考リンク

Aparapi
aparapi GitHub レポジトリ

実行方法

ライブラリのインストール

aparapiのライブラリをダウンロードします
aparapip5
スクリーンショット 2016-12-24 22.53.52.png

ダウンロードしたaparapip5-master.zipを解凍すると次のような階層になると思います。

スクリーンショット 2016-12-24 22.58.38.png

processingにライブラリをインストールします。
processingのユーザディレクトリを開き、次の階層になるように配置します。

スクリーンショット 2016-12-24 23.03.05.png

processingを起動し直すとロードされます。
サンプルにaparapiが表示されるようになります。

スクリーンショット 2016-12-24 23.07.39.png

ライブラリにもaparapiが登録されるようになります。

スクリーンショット 2016-12-24 23.11.09.png

プログラム

内容

スクリーンショット 2016-12-24 23.20.42.png

初期状態では利用可能であればOpenCL(GPUを利用した実装)で動きます、
クリックするたびにProcessing標準の動きとOpenCLを利用した動きが切り替わります。

動かした動画はvimeoにて公開しております

ソースコード

p5advent2016.pde
import com.amd.aparapi.*;

KernelP5 kernel;

long generations = 0;
long start = System.currentTimeMillis();

public void setup() {
  size(1280, 768);
  frameRate(240);
  background(0);

  fill(255);
  loadPixels();

  kernel = new KernelP5(width, height, pixels);
  //lifeKernel.setExecutionMode(Kernel.EXECUTION_MODE.CPU);
  updatePixels();
}
public void draw() {
  //background(0);
  kernel.nextGeneration();
  updatePixels();

  fill(0);
  rect(0, 0, 100, 20);
  fill(255);
  text(frameRate, 10, 10);
  println(this.kernel.getExecutionMode());
}

public void mouseMoved() {
  kernel.mx = mouseX;
  kernel.my = mouseY;
}

boolean GPUUsageflag = true;
public void mousePressed() {
  if (GPUUsageflag==true) {
    kernel.setExecutionMode(Kernel.EXECUTION_MODE.JTP);
  } else {
    kernel.setExecutionMode(Kernel.EXECUTION_MODE.GPU);
  }
  GPUUsageflag = !GPUUsageflag;
  println(GPUUsageflag);
}
KernelP5.pde
public class KernelP5 extends Kernel {

  private int[] imageData;
  private final Range range;
  private final int width;
  private final int height;
  private float time;
  private float timed;

  public int mx = 0;
  public int my = 0;

  public KernelP5(int _width, int _height, int[] pixels) {
    width = _width;
    height = _height;
    imageData = pixels;
    range = Range.create(width * height);
    println(getExecutionMode());

    setExplicit(true); 
    clear();
  }

  public void setImage(int[] pixels) {
    imageData = pixels;
  }

  @Override
    public void run() {
    int gid = getGlobalId();

    int tx = gid % width;
    int ty = gid / width;
    int red = 0;
    int green = 0;
    int blue = 0;
    for (int i = 0; i < 12; i++) {  
      float xx = cos(toRadians(i * 30) + (time)) * my + width / 2;
      float yy = sin(toRadians(i * 30) + (time)) * my + height / 2;
      float dx = tx - xx;
      float dy = ty - yy;

      float dx2 = dx * dx;
      float dy2 = dy * dy;

      float dist = sqrt(dx2 + dy2);

      if (dist > 512) {
        continue;
      } else {
        dist = (512 - dist) / 2;
      }
      float r = 0;
      r = abs(sin((mx / 500.0) * toRadians(dist * 5) + -time * 3) * dist);
      if (i % 3 == 0) {
        red += r;
      } else if (i % 3 == 1) {
        green += r;
      } else {
        blue += r;
      }
    }

    red = min(red, 255);
    green = min(green, 255);
    blue = min(blue, 255);
    imageData[gid] = 0xEE000000 + (red << 16) + (green << 8) + (blue);
  }

  public void nextGeneration() {
    time -= abs(sin(toRadians(timed))) * 0.01;
    timed += 1;
    execute(range);
  }

  public int[] getImageData() {
    return imageData;
  }
}

解説

aparapiを利用した実装はKernelの無名クラスを作る方法と

test.pde
        final float inA[] = new float[]{1.0f, 2.0f, 3.0f, 4.0f};
        final float inB[] = new float[]{0.1f, 0.2f, 0.3f, 0.4f};
        final float result[] = new float[inA.length];

        Kernel kernel = new Kernel() {
            @Override
            public void run() {
                int i = getGlobalId();
                result[i] = inA[i] + inB[i];
            }
        };

        Range range = Range.create(result.length);
        kernel.execute(range);

Kernelクラスを継承して作る方法があります。

test2.pde
public class FFTKernel extends Kernel {
    final float inA[] = new float[]{1.0f, 2.0f, 3.0f, 4.0f};
    final float inB[] = new float[]{0.1f, 0.2f, 0.3f, 0.4f};
    final float result[] = new float[inA.length];

    @Override
    public void run() {
        int i = getGlobalId();
        result[i] = inA[i] + inB[i];
    }
}

run()の中の実装が実行時にGPU(OpenCL)を利用するコードに変換されます。