忍者ブログ

Memeplexes

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

C#でGPGPUチュートリアル その1 GPUからCPU側へのデータ転送

SlimDX

C#でGPGPUを使うにはいくつか方法があると思います。
DirectXかOpenCL、CUDAをラップして使うなどです。

ここではDirectXを使うということにしておきます。
DirectXにはオープンソースのSlimDXというよくできたラッパーライブラリーが存在します。

MicrosoftもWindows API Code Pack for .NET Frameworkというラッパーを公開していますが、
現時点ではSlimDXのほうが上手くラッパーできているように思えます。

たとえばWindows API Code Pack for .NET FrameworkではIntPtrでしかアクセス出来ないデータが、
SlimDXではSystem.IO.Stream(か、それを継承したクラス)になっていたりといった具合です。
MSのラッパーでは、unsafeコードやMarshalクラスの使用を強いられることになるのです。
それよりはSlimDXのほうがスマートです。
少なくとも現時点では。

というわけで、今までのWindows API Code Pack for .NET Frameworkの使用はなかったことにして
ここからはSlimDXを使うことにします

SlimDXのインストール

インストールはSlimDXのページの上にある「Download」リンクを押して、
「DeveloperSDK」という項目の
「Install Developer SDK」という所をクリックすればいいようです。

howToDownloadSlimDX.jpg


ダウンロードされたファイル(SlimDX SDK (June 2010).exe)を実行すると
どうやら自動的にアセンブリが追加され使えるようになります。

インストールが終わると
Visual C# 2010の参照の追加ダイヤログに、
以下のように出てきます。

SlimDXAssembliesInstalled.jpg


なぜかSlimDXという名前のものが2つありますね。
これはx86かx64かという違いです。
OSが64bitならx64の方を選びたいところです。


DirectX SDKのサンプル、BasicCompute11を簡単に

SlimDXをインストールできたら早速なにかやってみましょう。
なにか・・・といきなりいわれても何をすればいいのかよくわかりませんので
本家DirectXのSDKに付いているサンプルを簡単に再現してみるのがいいでしょう。
ここではBasicCompute11というのを少しずつやってみたいと思います。

BasicCompute11はDirectX11で新たに導入された演算シェーダーを使った簡単なサンプルで、
GPUを使って足し算を行います

もちろんただの足し算ではなく、2つのベクトルの足し算です。

howBasicCompute11Works.jpg

buffer0に入れた数字と、buffer1に入れた数字を足し合わせ
bufferResultに格納するのです。
上の図では図を分かりやすくするために0~7まで8つの数字を書いていますが、
実際のサンプルでは1024個もの数字!(といってもその程度の数GPUにとってはなんてこと無いのでしょう)を足しあわせています。

さてこのサンプルは一見簡単そうですが、C++のコードだけで700行ちかくもあるという大きなプログラムです。
HLSL側も70行くらいあります。

簡単なはずのサンプルが難解というのはDirectXのサンプルではよくあることです。
そこでこのサンプルがやっていることを少しずつ、チュートリアル風にしてやっていきます。
まずは計算結果のbufferResultをCPU側に読み戻す事を考えましょう。
計算結果はGPUにあるため直接C#のコードで扱うことは出来ないのです。

GPUからCPU側へデータを送る

GPGPUではGPUで計算をし、計算結果もGPU内に格納されます。
ですから、そのままでは計算結果をCPU側で動くプログラムから扱えません。
GPUにある計算結果をCPU側に送ることが必要となります。

まずは、CPUからGPUに「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というデータを送り、
それをまたGPUからCPUへ戻すだけのプログラムを書いてみます。
using System.Collections.Generic;
using System.Linq;

using SlimDX;
using SlimDX.Direct3D11;

class Program
{
    static void Main(string[] args)
    {
        Device device = new Device(DriverType.Hardware);
        Buffer buffer = createCpuAccessibleBuffer(device, Enumerable.Range(0, 10).ToArray());

        int[] readBack = readBackFromGpu(buffer);

        foreach (var number in readBack)
        {
            System.Console.WriteLine(number);
        }
    }

    private static Buffer createCpuAccessibleBuffer(Device device, int[] initialData)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                CpuAccessFlags = CpuAccessFlags.Read,
                Usage = ResourceUsage.Staging
            }
            );
    }

    static int[] readBackFromGpu(Buffer from)
    {
        DataBox data = from.Device.ImmediateContext.MapSubresource(
            from,
            MapMode.Read,
            MapFlags.None
            );
        return getArrayInt32(data.Data);
    }

    static int[] getArrayInt32(DataStream stream)
    {
        int[] buffer = new int[stream.Length / sizeof(int)];
        stream.ReadRange(buffer, 0, buffer.Length);
        return buffer;
    }
}

コードはこれだけです。
GPUで計算はしていないので、HLSLファイルなどは必要ありません。
 
実行結果はこうなります。
0
1
2
3
4
5
6
7
8
9
ここで表示しているのはGPUにあるバッファの内容をCPU側に写しとった配列です。
きちんと、はじめにGPU側に渡してやった
「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というデータが戻ってきています。


Deviceクラス

このプログラムで最初に行ったのはSlimDX.Direct3D11.Deviceクラスの初期化です。
このクラスはGPUを制御するクラスです。
GPUにバッファを作りたい、計算させたい、というときに大抵必要になります。
public class Device : ComObject
コンストラクタは8個用意されていますが、今回必要なのは
一番シンプルなこのコンストラクタです。
public Device(DriverType driverType);

deviceTypeはデバイスの種類で、つぎの6つの中から選びます。

DeviceTypeの値 説明
Unknown タイプが不明
Hardware Direct3Dの機能を持つハードウェアです。まずほとんどのばあいこれを使ってDeviceクラスを初期化することになります。ハードウェアを使うためかなり速いです。というかこれ以外では遅すぎてまず通常目的では使えません。これを使いましょう。
Reference リファレンスドライバです。リファレンスドライバはソフトウェアで動いているため遅いです。遅いです―が正確で、デバッグ時やDirectXの機能をテストするというようなときには悪くはないのかもしれません。ただ、実際のアプリケーションで使う為のものではありません。
Null Nullドライバです。これはリファレンスドライバから描画機能がなくなったようなものです。これもデバッグ用で、まず普通のアプリケーションで使うためのものではありません。
Software Softwareドライバです。これは完全にソフトウェアで実装されています。やはり遅いため普通のアプリケーションで使うためのものではありません。
Warp WARPドライバです。これもソフトウェアで動きますが、パフォーマンスは高いです。

DeviceクラスにはGPGPUを行う上で重要なプロパティがあります。
ImmediateContextプロパティです。
デバイスの状態にアクセスするときには大抵このプロパティを通します。

public DeviceContext ImmediateContext { get; }

と、いうよりDeviceクラスはほとんど抜け殻のようなもので、
重要な機能はだいたいここに入っています。
たとえばXNAのGraphicsDeviceのメソッドに相当するものは多くがこの中に入っています。

Bufferクラス

Deviceクラスを初期化した後は
CPUからのデータを使ってGPUでSlimDX.Direct3D11.Bufferクラスのインスタンスを生成しています。
public class Buffer : Resource
このバッファというものはGPUに存在するデータの塊のようなもので、
頂点バッファであったりインデックスバッファであったり、定数バッファであったりします。

今回はシェーダーを動かさないため、「これは~~バッファだ」とか言うようなバッファの種類に気を使う必要はありません。

コンストラクタは6種類用意されています。
今回使っているのは2番目に簡単な(1番簡単なものはバッファの初期データを渡せないのです)
このコンストラクタです。
public Buffer(Device device, DataStream data, BufferDescription description);
deviceはこのバッファを作るデバイスです。

dataはバッファの初期データ。今回の場合は「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というデータです。
DataStreamというのはSystem.IO.Streamを継承したクラスです。

descriptionはこのバッファのサイズやバッファの種類などの情報です。
有り得そうなコンストラクタ引数はだいたいここにまとめられていると考えて構いません。
実際、この中身を直接コンストラクタ引数としたコンストラクタも存在します。


DataStreamクラス

Windows API Code Pack for .NET Frameworkの欠点の一つは
APIに渡すデータをIntPtrでしか渡せないことでした。
これではユーザーにunsafeやらMarshalやらネイティブとの境目を意識させるような機能を使えと言っているようなもので、
C#のAPIとしてはあまり気持ちのいいものではありません。

それに対してSlimDXではIntPtrの代わりに
System.IO.Streamを継承したSlimDX.DataStreamクラスを使い、境界を意識する必要はありません。
純粋なC#の世界だけで完結できるのです。

public class DataStream : Stream, IDisposable
コンストラクタは3種類用意されていますが、
今回使うのは次の初期値の配列を引数に取るコンストラクタです。
public DataStream(Array userBuffer, bool canRead, bool canWrite);

userBufferはこのインスタンスが初期値に使う配列です。
canReadはこのストリームから読み込むことができるかどうか、です。(Stream.CanReadと関係があるのでしょうか)
canWriteはこのストリームに書き込むことができるかどうか、です。(Stream.CanWriteと関係があるのでしょうか)

DataStreamはマネージ側に配列を持ってくるメソッドも定義しています。

public T[] ReadRange(int count) where T : struct; public int ReadRange(T[] buffer, int offset, int count) where T : struct; 

bufferは読み込んだ結果を格納する配列。
offsetは読み込みを開始するインデックス。
countはよみこむ要素数。
戻り値は読み込んだ要素数です。

BufferDescriptionクラス

Bufferクラスにはコンストラクタに大量の引数を用意したくないためか
たくさんのオプションをメンバーに持ったSlimDX.Direct3D11.BufferDescriptionを引数に取ります。
BufferDescriptionはBufferのインスタンスを生成するのに必要ないろいろな情報を格納します。

プロパティ名 説明
BindFlags BindFlags パイプライン中でどのように使われるか、を表します。 たとえば頂点バッファとして使われるか?インデックスバッファとして使われるか?定数バッファとして使われるのか?シェーダーのリソースとして使われるのか?ストリームの出力として使われるのか?レンダーターゲットとして使われるのか?デプスステンシルバッファとして使われるのか?アンオーダードアクセス(複数のスレッドから同時に読み書き可能な)リソースとして使われるのか? ・・・・・・などです
CpuAccessFlags CpuAccessFlags このバッファにCPUがいかほどのアクセス権限を持っているかを表します。None, Write, Readの3種類があります。
OptionFlags ResourceOptionFlags その他のオプションです。 たとえば、MipMapを生成するか?複数のデバイス間で共有されるか?TextureCubeか?インスタンシングを有効にするか?RawBufferか?StructuredBufferか?深度バイアスはどうするか?KeyedMutexか?GDIと互換性があるか? ・・・・・・などです。
SizeInBytes int バッファが何バイトか、を表します。
StructureByteStride int StructuredBufferとして使用する場合(OptionFlagsでそのように指定するのですが)に使います。ひとつの構造体のサイズが何バイトかを表します。
Usage ResourceUsage バッファの使われ方を表します。GPUが読み書きのできるDefault、GPUで読み取りのみ可能なImmutable、GPUからは読み取りのみCPUからは書き込みのみのDynamic、GPUからCPUへデータを転送できるStagingの4つです。

今回はGPUからCPUへ転送が可能になるように、
CpuAccessFlagsはReadに、
UsageはStagingにしたわけです。


DeviceContextクラス

このクラスは解説が最後らへんになってしまいながらも一番大切なクラスです。
今回のサンプルではたいしたことはしていませんが、それでも重要です。
Deviceクラスはこれのために存在すると言ってもいいくらいです。
実際、このSlimDX.Direct3D11.DeviceContextクラス
描画のための命令全般を司っています。

今回このクラスがやってくれたことは、GPU上に存在するバッファを
CPUがアクセス可能なメモリに持ってきてくれたことです。
これがなければせっかくGPGPUをやっても結果をConsole.WriteLine()で確認できません。
public DataBox MapSubresource(
        Resource resource,
        MapMode mode, 
        MapFlags flags);
resourceはメモリに展開するリソース、今回の場合はバッファです。
modeはCPUの読み書き許可です。
flagsはこのメソッドを実行するに当たってGPUが忙しい状態だった場合どうするかです。

modeをもう少し詳しく言うとこうなります:
MapModeの名前 説明
Read リソースは読み取り用にマップされます。ただしリソースが読み取りアクセス可能なように生成されていなければいけません。
Write リソースは書き込み用にマップされます。ただしリソースが書き込みアクセス可能なように生成されていなければいけません。
ReadWrite リソースは読み書き用にマップされます。ただしリソースが読み書きアクセス可能なように生成されていなければいけません。
WriteDiscard リソースは書き込み用にマップされますが、以前の情報は未定義になります。ただしリソースが書き込みアクセス可能なように生成されていなければいけません。
WriteNoOverwrite リソースは書き込み用にマップされますが、リソースのすでに存在しているコンテンツは上書きできません。このフラグは頂点バッファとインデックスバッファでのみ有効です。リソースは書き込みアクセス可能なように生成されていなければいけません。


flagsをもう少し詳しく言うとこうなります:
 
MapFlagsの名前 説明
None リソースが使えるようになるまで待ちます。
DoNotWait リソースが使えるようになるまで待ちません。GPUがCPUからのアクセスをブロックするようであればWasStillRenderingを返します。

戻り値のDataBoxはCPUからアクセス可能なメモリへの参照を持っています。

 

DataBoxクラス

SlimDX.DataBoxクラスはデータを3次元で保持するクラスです。
ただし今回の場合バッファは1次元ですしあまり意味はありません。
実質的にDataStreamのラッパーのようなものです。

持っているデータへアクセスはDataBox.Dataプロパティを使います。
public DataStream Data { get; }
DataはSystem.IO.Streamを継承したSlimDX.DataStreamクラスです。
 




拍手[11回]

PR