忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

[XNA] GPUを使って計算した結果をCPUに戻す

なんとなくGPUを使った計算結果をCPU側でも使ってみたくなったので、やってみました。

ただ、これはXbox360ならともかくパソコンではパフォーマンスが悪くなるそうですね。
グラフィックスメモリをCPUからは読むのはあまり速くないとかだそうです。
ですからよほど面白いことができない限り、控えるべきでしょう。

実際に何かに使うとしたら、並列的な計算でしょうね。
GPUは、CPUと違って並列的な計算が得意ですから、そこら辺で面白いことができるに違いありません。
ある研究室でGPUを使ってニューラルネットワークを動かし、人の顔を判別している研究を見たことがありますが、まあそんな感じでしょうか。
その研究では、CPUを使った時よりも、何倍も計算速度が速くなったそうです。
こういうのはCPUよりもGPUのほうが向いているんですね。

もちろんニューラルネットワークは単なる一例で、「この手があったか!」みたいな応用方法が他にもあるに違いありません。
そしてその応用方法の中には、ゲームをもっと面白くする何かがあるにちがいありません!(たぶん)

というわけでXNAを使ってやってみました。

かといって、いきなりGPUでニューラルネットワークを動かしたりというのはあまりにハードルが高すぎるので、まずは一番簡単な計算をしてみたいと思います。

つまり、1 + 1です。
たぶん人が一番簡単だと考える計算ではないでしょうか!
たぶん幼稚園児でもできます。
これをGPUに計算させて、CPU側に戻し、ウィンドウに「2」と表示するのです。
「1 + 1なんてGPU使わなくたってわかるよ!」って感じですが、話をシンプルにするにはこれが一番です。

MyGame.cs


using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class MyGame : Game
{
    GraphicsDeviceManager graphics;

    Texture2D inputTexture;
    RenderTarget2D result;
    Effect additionCalculator;
    VertexDeclaration vertexDeclaration;

    //GPUから計算結果を受け取るバッファ
    float[] resultBuffer = new float[2];

    VertexPositionTexture[] vertices = {
            new VertexPositionTexture(new Vector3(-1, 1, 0), new Vector2()),
            new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1, 0)),
            new VertexPositionTexture(new Vector3(-1, -1, 0), new Vector2(0, 1)),
            
            new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1, 0)),
            new VertexPositionTexture(new Vector3(1, -1, 0), new Vector2(1, 1)),
            new VertexPositionTexture(new Vector3(-1, -1, 0), new Vector2(0, 1))
        };


    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void LoadContent()
    {
        inputTexture = new Texture2D(GraphicsDevice, 2, 1, 0, TextureUsage.None, SurfaceFormat.Single);
        inputTexture.SetData<float>(new float[] { 1, 1 });

        result = new RenderTarget2D(GraphicsDevice, 2, 1, 0, SurfaceFormat.Single);

        additionCalculator = Content.Load<Effect>("AdditionCalculator");


        GraphicsDevice.VertexDeclaration = new VertexDeclaration(
            GraphicsDevice,
            VertexPositionTexture.VertexElements
            );
        vertexDeclaration = GraphicsDevice.VertexDeclaration;
    }

    protected override void UnloadContent()
    {
        inputTexture.Dispose();
        result.Dispose();
        vertexDeclaration.Dispose();
    }

    protected override void Update(GameTime gameTime)
    {
        Window.Title = resultBuffer[1].ToString();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.SetRenderTarget(0, result);

        additionCalculator.Parameters["InputTexture"].SetValue(inputTexture);
        additionCalculator.Parameters["InputWidth"].SetValue(2);
        additionCalculator.Begin();
        additionCalculator.CurrentTechnique.Passes[0].Begin();

        GraphicsDevice.DrawUserPrimitives<VertexPositionTexture>(
            PrimitiveType.TriangleList,
            vertices,
            0, 2
            );

        additionCalculator.CurrentTechnique.Passes[0].End();
        additionCalculator.End();

        //GetDataを呼ぶには、レンダーターゲットはいったん
         //デバイスから外されなければいけません。
        GraphicsDevice.SetRenderTarget(0, null);

        result.GetTexture().GetData<float>(resultBuffer);
    }

    static void Main()
    {
        using (MyGame game = new MyGame())
        {
            game.Run();
        }
    }
}


AdditionCalculator.fx(Contentフォルダ内)
float InputWidth;
texture InputTexture;

sampler InputSampler = sampler_state
{
    Texture = (InputTexture);
};

struct VertexPositionTexture
{
    float4 Position : POSITION;
    float2 TextureCoordinate : TEXCOORD;
};

VertexPositionTexture VertexShaderFunction(VertexPositionTexture input)
{
    return input;
}

float4 PixelShaderFunction(VertexPositionTexture input) : COLOR0
{
    return tex2D(InputSampler, input.TextureCoordinate + float2(-1/InputWidth, 0))
		+ tex2D(InputSampler, input.TextureCoordinate);
}

technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

AdditionByGPU.jpg

できました。
やったー!

きちんと、ウィンドウのタイトルに"2"と表示されているのがわかると思います(もちろん1 + 1の結果ですよ)。
GPUに、{1, 1}という配列をテクスチャとして送り込んで、ピクセルシェーダーで足し算しているのです。
そして、描画結果を2x1のテクスチャとしてCPU側に戻します。
そのデータをGetDataメソッドで読み込み、ウィンドウのTitleプロパティにセットすれば完了です。



additionByGPUDiagram.jpg

これを1秒間に60回繰り返しています。
最初の{1, 1}を送り込むのは一度だけですが、1 + 1は何度も何度も、60ヘルツで計算しています。
なんせDrawメソッド内でやっていますからね。

念のため、SetDataメソッドの引数をいろいろ変えて検証してみました。

inputTexture.SetData<float>(new float[] { 1, 2 });

a6d637c6.jpg

3か・・・・・・1 + 2は3ですから、うまくいっているようです。
つぎは2 + 5くらいをやってみましょう

inputTexture.SetData<float>(new float[] { 2, 5 });

9b78bdf9.jpg

2 + 5は7ですから、大丈夫ですね。

どうやらこれでうまくいっているようです。
ここでは単なる足し算の計算をしましたが、現実にはもっとエキサイティングでファンタスティックなことをやることができるでしょう。

拍手[2回]

PR