忍者ブログ

Memeplexes

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

C#でDirectX11 SlimDXチュートリアルその01 メッセージループ

 C#でDirectX11を使いたい時、
最も便利な方法はSlimDXです。

SlimDXのインストールは大して難しくありません

ここではSlimDXを使って3DCGを表示する方法を
メモしておこうと思います。

まずは描画ループの話から。
これはこのページが元になっています。


メッセージループ

DirectXで3DCGを表示する場合、
1秒に何十回も3DCGをウィンドウに繰り返し描画する事になります。
そのため普通のWindowsアプリケーションとはプログラムの構造が異なります。

メッセージループが普通のものと少し違うのです。
まずは普通のWindowsアプリケーションを見てみましょう:


普通のウィンドウ

using System.Windows.Forms;
 
class Program
{
    static void Main()
    {
        using (Form form = new Form())
        {
            Application.Run(form);
        }
    }
}
slimDXWindow00.jpg

Formインスタンスを作り、それをApplication.Runの引数に渡すと、
Formが可視状態になった後、
Runメソッドの中でメッセージループがスタートします。

キーが入力された、マウスが動いた、描画が必要、
などのメッセージがループの中で処理され、
結果としてウィンドウが表示されるのです。


ペイントループ(非推奨)

上の例のように、Formは描画をあまり行いません。
まず描画を行い、その後ウィンドウに表示している内容を変更する必要が生じるまで
(例えば△を表示していたけれどこれからは○を表示しなければならなくなったなど)
描画は行わないのです。
描画するタイミングは場合によって異なります。
10秒に一回の描画かもしれませんし、10分に一回かもしれません。

一方、ゲームなどでめまぐるしく表示内容が変わる場合、
1秒に何十回と描画しなければなりません。
それが普通のDirectXアプリケーションです。

そうする方法の一つとして、
Paintイベントが実行された後、直ぐにまた次のPaintイベントを起こす
という方法があります。

class Program
{
    static void Main()
    {
        using (Game game = new Game())
        {
            game.Run();
        }
    }
}
 
class Game : System.Windows.Forms.Form
{
    protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
    {
        base.OnPaint(e);
        Draw();
        Invalidate();
    }
 
    protected virtual void Draw()
    {
        //draw 3DCG
    }
 
    public void Run()
    {
        System.Windows.Forms.Application.Run(this);
    }
}
 
実行結果は一見上の例と同じです。
ただ上と違って、Drawメソッドが1秒に何度も呼び出されます。
(Drawの中でSystem.Console.WriteLineを呼び出せば、
VisualStudioのOutputウィンドウでその様子がわかります)

PaintイベントでOnPaintが実行されますが、
そのなかでInvalidate()を呼び次のPaintイベントが即座にスタートしているのです。

この方法には2つの欠点があります。

ひとつは、Invalidate()は実はメモリを少し消費しているということです。
ガベージコレクタが動く頻度が上がってしまいます。
パフォーマンス上の問題があるのです。

2つ目は、Paintイベントは1秒になんども呼び出されるといった用法を想定されていないということです。
特にPaintイベントはOSが知っているものなので、濫用は禁物です
こちらにもパフォーマンス上の問題があります。


Application.DoEvents()(非推奨)

二つ目の方法は、多くのサンプルで使われる方法ですが、
メッセージループを明示的に書いてしまう方法です。
Application.Run()を使わずその中身を直接書いてしまいます。

class Program
{
    static void Main()
    {
        using (Game game = new Game())
        {
            game.Run();
        }
    }
}

class Game : System.Windows.Forms.Form
{
    public void Run()
    {
        this.Show();

        while (Created)
        {
            Draw();
            System.Windows.Forms.Application.DoEvents();
        }
    }

    protected virtual void Draw()
    {
        //draw 3DCG
    }
}
 
この方法の良いところは、Paintイベントがなんども実行されるなんてことが無くなっているというところです。
OSに優しくなっています。

しかし欠点も残っていて、Tom Miller氏によれば、DoEvents()もやはりメモリを毎回消費します。
ガベージコレクションが起動する頻度が高くなり、
パフォーマンス上の問題があります。


P/Invokeを使う(解決編)

メモリ消費問題を解決するには、
もうWin32APIを直接使うしかありません。
P/Invokeを使いコードを書き、その間ヒープメモリを消費しないように気をつけるのです。
 
この方法はTom Miller氏のブログに書かれていたものです。

class Program
{
    static void Main()
    {
        using (Game game = new Game())
        {
            game.Run();
        }
    }
}

struct Message
{
    public System.IntPtr hWnd;
    public uint msg;
    public System.IntPtr wParam;
    public System.IntPtr lParam;
    public uint time;
    public System.Drawing.Point pt;
}

class Win32Api
{
    [System.Security.SuppressUnmanagedCodeSecurity] //for performance
    [System.Runtime.InteropServices.DllImport("user32.dll")]
    public static extern bool PeekMessage(
        out Message msg,
        System.IntPtr hWnd,
        uint messageFilterMin, 
        uint messageFilterMax,
        uint flags
        );
}

class Game : System.Windows.Forms.Form
{
    bool AppStillIdle
    {
        get
        {
            Message msg;
            return !Win32Api.PeekMessage(out msg, System.IntPtr.Zero, 0, 0, 0);
        }
    }

    public void Run()
    {
        System.Windows.Forms.Application.Idle += delegate
        {
            while (AppStillIdle)
            {
                Draw();
            }
        };
        System.Windows.Forms.Application.Run(this);
    }

    protected virtual void Draw()
    {
        //draw 3DCG
    }
}
 
この方法は他の方法と比べてややこしいですが、
もっともメモリにやさしいです。
アプリケーションがアイドル状態になっている間、
描画し続けます。

SlimDX付属のMessagePumpクラスを使う

上のP/Invoke法は良い方法なのですが、ややこしいです。
さいわい、SlimDXにはこの方法をカプセル化したクラスが存在します。
SlimDX.Windows.MessagePumpです。
public sealed class MessagePump

MessagePumpクラスには、Application.Runのようなメソッドがあります。
Application.Runとの違いは、アイドル状態になったときに実行するメソッドを引数として取るというところです。

public static void Run(Form form, MainLoop mainLoop);

formは描画を行うウィンドウです。
mainLoopは描画を行うメソッドです。このメソッドが適切なタイミングに呼ばれるのです。

SlimDX.Windows.MainLoopは引数も戻り値も存在しないデリゲートです。

public delegate void MainLoop();

形はSystem.Actionと同じですね。

これらのラッパーを使うと、P/Invoke法はこうなります:

class Program
{
    static void Main()
    {
        using (Game game = new Game())
        {
            game.Run();
        }
    }
}
 
class Game : System.Windows.Forms.Form
{
    public void Run()
    {
        SlimDX.Windows.MessagePump.Run(this, Draw);
    }
 
    protected virtual void Draw()
    {
        //draw 3DCG
    }
}
 


 

拍手[9回]

PR

[コンピュートシェーダー] groupsharedとGroupMemoryBarrierWithGroupSync()

スレッドグループ

GPGPUに限った話ではありませんが、
何か並列計算していると、
別のスレッドが計算を終わってから次の計算をしたい、
ということが良くあります。

次の計算が別のスレッドの計算結果に依存するような場合です。

そんなときに役に立つのがHLSLの組み込み関数、
GroupMemoryBarrierWithGroupSync()関数です。

void GroupMemoryBarrierWithGroupSync();

これを使うと、コンピュートシェーダーの他のスレッドの計算が終わるまで待ちます。
別の言い方をすると、スレッドグループ内のすべてのスレッドが
これの呼び出しに到達するまで実行をブロックします。

ただしこの関数はどんな時にも使えるというわけではなくて、
コンピュートシェーダーでgroupsharedな変数にアクセスしている場合に限られます。

groupsharedは変数の記憶域修飾子で、
これがついた変数はスレッドグループ内で共有されます。

例えばこんなふうに使います。

groupshared int data[100];

こうなっていると、dataはスレッドグループ内で共有され、
これに対するアクセスは
GroupMemoryBarrierWithGroupSync()の保護の対象となります。

なお、groupsharedな変数はコンピュートシェーダーの
スレッドグループ内で共有されているため、
アクセスするときのインデックスには
SV_DispatchThreadIDよりもSV_GroupIndexを使うほうがいいでしょう。

SV_GroupIndexはグループ内ごとのスレッドのインデックスです。
(別グループのスレッドとは値が衝突することはありますが、
自分のグループの他のスレッドとは衝突しません)
詳しくはMSDNのページによい図があります。


サンプルコード

まずは、GroupMemoryBarrierWithGroupSync()を使った時と使わないとき
の違いをはっきりさせたいと思います。

C#側はこうです。
こちらはHLSLとは関係ありませんので固定です。
これはHLSLで計算した結果を表示する、というプログラムです。
C#側からHLSLに入力として何かデータを与えたりはしません。

Program.cs

using System.Collections.Generic;
using System.Linq;

using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.D3DCompiler;

class Program
{
    static Buffer bufferSum;

    static void Main(string[] args)
    {
        Device device = new Device(DriverType.Hardware);

        bufferSum = createStructuredBuffer(
            device, 128 * sizeof(int), BindFlags.UnorderedAccess
            );

        initComputeShader(device);
        device.ImmediateContext.Dispatch(1, 1, 1);
        writeBuffer(device, bufferSum);
    }

    static void initComputeShader(Device device)
    {
        device.ImmediateContext.ComputeShader.SetUnorderedAccessView(
            new UnorderedAccessView(device, bufferSum), 0
            );

        ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
            "MyShader.fx",
            "MyComputeShader",
            "cs_5_0",
            ShaderFlags.None,
            EffectFlags.None
            );
        device.ImmediateContext.ComputeShader.Set(
            new ComputeShader(device, shaderBytecode)
            );
    }

    static void writeBuffer(Device device, Buffer buffer)
    {
        Buffer cpuAccessibleBuffer = createCpuAccessibleBuffer(device, buffer.Description.SizeInBytes);
        device.ImmediateContext.CopyResource(buffer, cpuAccessibleBuffer);
        int[] readBack = readBackFromGpu(cpuAccessibleBuffer);

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

    static Buffer createStructuredBuffer(Device device, int sizeInBytes, BindFlags bindFlags)
    {
        return new Buffer(
            device,
            new BufferDescription
            {
                SizeInBytes = sizeInBytes,
                BindFlags = bindFlags,
                OptionFlags = ResourceOptionFlags.StructuredBuffer,
                StructureByteStride = sizeof(int)
            }
            );
    }

    static Buffer createCpuAccessibleBuffer(Device device, int sizeInBytes)
    {
        return new Buffer(
            device,
            new BufferDescription
            {
                SizeInBytes = sizeInBytes,
                CpuAccessFlags = CpuAccessFlags.Read,
                Usage = ResourceUsage.Staging
            }
            );
    }

    static int[] readBackFromGpu(Buffer from)
    {
        DataBox dataBox = from.Device.ImmediateContext.MapSubresource(
            from,
            MapMode.Read,
            MapFlags.None
            );
        int elementCount = (int)(dataBox.Data.Length / sizeof(int));
        return dataBox.Data.ReadRange<int>(elementCount);
    }
}

HLSL側はこうです。
GroupMemoryBarrierWithGroupSyncがあるのと無いのとでは
結果がなるべく違ってくるように書いています。
(もちろん、環境によって違うでしょうから、適宜変数を変えてみたりしてください)

MyShader.fx
RWStructuredBuffer<int> MyBuffer : register(u0);

static const int ElementCount = 128;
groupshared int data[ElementCount];

int getSum()
{
int result = 0;

for(int i = 0; i < ElementCount; i++)
{
result += data[i];
}

return result;
}

[numthreads(ElementCount, 1, 1)]
void MyComputeShader(uint groupIndex : SV_GroupIndex )
{
int time = 0;

//スレッドによって処理時間が変わる
for(int i = 0; i < groupIndex; i++)
{
time++;
}

data[groupIndex] = time;
//GroupMemoryBarrierWithGroupSync();
MyBuffer[groupIndex] = getSum();
}

さて私の環境では実行結果はこうなりました:

1951777886
1951777886
1951777886
1951777886
1951777886
1951777886
1951777886
1951777886
1951777886
..(中略)
1951777886
8128
8128
8128
8128
8128
8128
8128
8128
..(中略)
8128

このプログラムはある配列の中身の合計を、
複数のスレッドで計算すします。
複数のスレッドが扱うのは同じ配列ですから、
どのスレッドでも同じ結果になるとおもうかもしれません。
が、結果は見ての通り、19517778868128の2種類の違う値になってしまっています。
 
その原因は(これはわざとそうなるよう仕組んでいるのですが)、
スレッドごとに計算を開始する時間がかなり異なっているからです。
あるスレッドは配列の初期化が終わる前に合計の計算を開始するために1951777886というような値を出します。
一方で、別のスレッドは配列の初期化が終わった後に計算を開始するため8128という、正しい値が出るのです。
 
GroupMemoryBarrierWithGroupSync()のコメントアウトをとってみてください。
こんどは、全て8128という数になるはずです。
GroupMemoryBarrierWithGroupSync()によって全てのスレッドが
配列の初期化が終わるまで待つようになるのです。


拍手[1回]


C#でGPGPUチュートリアル その5 入力にShaderResourceViewを使う

 ShaderResourceViewを使う意味は?

前回はGPGPUの計算に3つのバッファを使用しました。
2つのバッファを入力にして、1つのバッファに書き込んだのです。
この時に3つのバッファを全てUnorderedAccessViewにしました。

basicCompute11WithAllBuffersUnorderedAccessView.png

ところが、同じ計算をやっているDirectX SDKのサンプルでは、
入力の2つはShaderResourceViewにしています。
(出力はUnorderedAccessViewですが)

basicCompute11WithInputsShaderResourceView.png

なぜShaderResourceViewを使うのでしょうか?
あるいは言い換えるなら、
なぜUnorderedAccessViewを避けるのでしょうか?
前回も言った気がしますが、これはおそらくUnorderedAccessViewが希少な存在だからです(たぶん)。
あるいはもしかしたらパフォーマンス的な面もあるかもしれません。
たとえばUnorderedAccessViewよりもShaderResourceViewのほうが、アクセスが早いというような。

UnorderedAccessViewが希少というのは、
"cs_4_0"では1つしかUnroderedAccessViewを使えないということです。
"cs_4_0"でコンパイルしようとすると例外をスローしてしまいます。
1つしか使えないのでは、前回のように入力2つに出力ひとつという、合計3つ必要とする計算はできません。
ですから前回は"cs_5_0"を使いました。
こちらはUnorderedAccessViewを8個まで使えます
それでも8個制限のうち3つも使ってしまうというのはあまりいい気はしません。

一方、ShaderResourceViewはというと、
限度はそれほどないようです。
少なくとも16個変数を作ってコンパイルした程度ではどうということはありません。
ただしShadereResourceViewは読み込み専用で、書込み先には使えないようです。
(msdnにはShader-Resourceビューはどのように読み込まれるかを指定する、というようなことが書いてあります。)

ですから、入力はShaderResourceView、出力はUnorderedAccessView
というふうに分けて使うといいのでしょう。

ShaderResourceViewを使う(C #側)

バッファをShaderResourceViewとして使うには、
Bufferのコンストラクタ引数の中で
BindFlags.ShaderResourceを指定しておく必要があります。

そうして作ったバッファのインスタンスを引数として、
SlimDX.Direct3D11.ShaderResourceViewクラスのインスタンスを作ります。

public class ShaderResourceView : ResourceView

public ShaderResourceView(Device device, Resource resource);

deviceはこのShaderResourceViewをつくるデバイス。
resourceはこのビューを関連付けるリソースです。今回はこれはバッファです。

こうして作ったビューは、ComputeShaderWrapper.SetShaderResources()メソッドを使って
デバイスにセットします。
public void SetShaderResources(
    ShaderResourceView[] resourceViews, 
    int startSlot, 
    int count
    );

resourceViewsはセットするビューの入った配列。
startSlotはGPU側のセットする最初のインデックス。
countはresourceViewsから何個セットするかを表す個数です。


ShaderResourceViewを使う(HLSL側)

C#側でShaderResourceViewを使うと決めた場合、
HLSL側ではStructuredBuffer<T>を使うことになります。
(前回まで使っていたのはRWStructuredBuffer<T>でしたが、RWがなくなっています。
どういう事かというと、読み込みしかできなくなった、ということです。
ですから計算結果を格納するバッファは前回までのようにRWの付いた方を使います。
入力バッファをStructuredBuffer<T>にするのです。)

StructuredBuffer<T>は読み取り専門ですが、
やはり演算子[]を使うことができます。
T Operator[](in uint indexPosition);

indexPositionは[]の中のインデックスです。

また、StructuredBuffer<T>の変数には: register(t0)をつけて、
シェーダーリソースとして扱ってやります。
(実はregister(t0)などと付けてやらなくてもシェーダーリソースとして扱われるのですが)
tはテクスチャを意味します。


コード

Program.cs

using System.Collections.Generic;
using System.Linq;

using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.D3DCompiler;

class Program
{
    static Buffer bufferSum;
    static Buffer buffer0;
    static Buffer buffer1;
    const int ElementCount = 20;

    static void Main(string[] args)
    {
        Device device = new Device(DriverType.Hardware);

        bufferSum = createStructuredBuffer(
            device,
            Enumerable.Range(0, ElementCount).ToArray(),
            BindFlags.UnorderedAccess
            );
        buffer0 = createStructuredBuffer(
            device,
            Enumerable.Range(0, ElementCount).ToArray(),
            BindFlags.ShaderResource
            );
        buffer1 = createStructuredBuffer(
            device,
            Enumerable.Range(0, ElementCount).ToArray(),
            BindFlags.ShaderResource
            );

        initComputeShader(device);
        device.ImmediateContext.Dispatch(ElementCount, 1, 1);
        writeBuffer(device, bufferSum);
    }

    static void initComputeShader(Device device)
    {
        device.ImmediateContext.ComputeShader.SetUnorderedAccessView(
            new UnorderedAccessView(device, bufferSum), 0
            );
        device.ImmediateContext.ComputeShader.SetShaderResources(
            new[]
            {
                new ShaderResourceView(device, buffer0),
                new ShaderResourceView(device, buffer1),
            },
            0,
            2
            );

        ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
            "MyShader.fx",
            "MyComputeShader",
            "cs_4_0",
            ShaderFlags.None,
            EffectFlags.None
            );
        device.ImmediateContext.ComputeShader.Set(
            new ComputeShader(device, shaderBytecode)
            );
    }

    static void writeBuffer(Device device, Buffer buffer)
    {
        Buffer cpuAccessibleBuffer = createCpuAccessibleBuffer(device, buffer.Description.SizeInBytes);
        device.ImmediateContext.CopyResource(buffer, cpuAccessibleBuffer);
        int[] readBack = readBackFromGpu(cpuAccessibleBuffer);

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

    static Buffer createStructuredBuffer(Device device, int[] initialData, BindFlags bindFlags)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                BindFlags = bindFlags,
                OptionFlags = ResourceOptionFlags.StructuredBuffer,
                StructureByteStride = sizeof(int)
            }
            );
    }

    static Buffer createCpuAccessibleBuffer(Device device, int sizeInBytes)
    {
        return new Buffer(
            device,
            new BufferDescription
            {
                SizeInBytes = sizeInBytes,
                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;
    }
}




MyShader.fx
RWStructuredBuffer<int> BufferSum : register(u0);
StructuredBuffer<int> Buffer0 : register(t0);
StructuredBuffer<int> Buffer1 : register(t1);

[numthreads(1, 1, 1)]
void MyComputeShader(uint3 threadID : SV_DispatchThreadID )
{
    BufferSum[threadID.x] = Buffer0[threadID.x] + Buffer1[threadID.x];
}

実行するとこうなります。

0
2
4
6
8
10
12
14
16
18
20
22
24
26
28
30
32
34
36
38


シェーダーリソースにするだけでは物足りないので
表示する数の数を2倍にしてみました。

このプログラムがやっていることは、上に書いたとおり、
2つのシェーダーリソースを作り、その中に入っている数を足しあわせ
1つのアンオーダードアクセスなバッファに書き込んでいるというものです。

basicCompute11WithInputsShaderResourceView.png



この図では0~7までですが、このプログラムでは0~19まで足しています。
なので出力は0~38になるというわけですね。


















拍手[0回]


C#でGPGPUチュートリアル その4 複数のリソースを計算に使う

ベクトルA + ベクトルB = 

 前回行った計算は
ひとつのバッファの数値を2倍して
もとのバッファに格納するというものでした
計算にはひとつのバッファしか使用していませんでした。

しかし現実の世界では、ひとつだけでなく
色々なデータを使って計算をするのが普通だと思います。

というわけで今回は、複数のバッファを使って計算してみましょう。

howBasicCompute11Works.jpg


3つのバッファを用意し、
2つのバッファを足した結果を
3つ目に格納します。

やっている計算はDirectX SDKのサンプル、
BasicCompute11とほぼ同じです。


複数のUnorderedAccessViewを演算シェーダにセットする : SetUnorderedAccessView()メソッド

今回は複数のバッファを使いますが、
それらすべてをUnorderedAccessViewとします。

本当は、DirectX SDKのサンプルを見ると
計算結果を格納するバッファ以外は
UnorderdAccessViewでは無くShaderResourceViewとしていて、
全部で2種類のViewを使っています。
が、ここではシンプルさを優先してUnorderedAccessViewだけを使って計算しましょう。

なお、DirectX SDKのサンプルで、
UnorderedAccessViewを必要最低限なところにしか使っていない理由は、
これが希少なものだから、というのがあるのだと思います(鵜呑みにしないでください!)。
実は、UnorderedAccessViewは"cs_4_0"では1つしか使えません
3つのバッファが登場する今回のケースでは本当はまずいのです。
今回のケースでは、3つのバッファをUnorderedAccessViewとして使うために
コンパイル時のシェーダープロファイルを"cs_5_0"にします。
"cs_5_0"の場合のUnorderedAccessViewの数の上限はなので、
今回の場合(3つ使う)も十分大丈夫です。

前回使ったComputeShader.SetUnorderedAccessView()メソッドでは
ひとつしかバッファ(のView)をセット出来ません。
今回は3つのViewをセットしなければいけないのですが、
それには前述のメソッド名の最後にsがついただけの、
ComputeShader.SetUnorderedAccessViews()メソッドを使います。

public void SetUnorderedAccessViews(
    UnorderedAccessView[] unorderedAccessViews, 
    int startSlot, 
    int count
    );

前述のメソッドとは違い引数は複数をセット出来るよう配列です。
unorderedAccessViewsはセットするUnorderedAccessViewの配列。
startSlotはセットを開始するデバイスのインデックス。
countは配列の中からセットする数です。

registerキーワード(HLSL)

さて、上のメソッドが想定していることは、
HLSL側にUnorderedAccessViewの配列のようなものがある、
ということですが、その順番はどうやって決まるのでしょうか?
それは基本的にはHLSLファイルの中で変数を宣言した順番です。
たとえば
 
RWStructuredBuffer<int> BufferSum;
RWStructuredBuffer<int> Buffer0;
RWStructuredBuffer<int> Buffer1;
とあるとき、
デバイスには3つの長さのUnorderedAccessViewの配列があり、
0番目はBufferSumで
1番目はBuffer0で
2番目はBuffer1です。

しかしこれはいかがなものでしょうか?
このままだと、たとえば変数宣言の位置を変えただけでプログラムが動かなくなってしまいます。
計算の入力に使用するバッファが計算結果の出力に使用されるかもしれません。

そういった事を防ぐには、変数の後にregister()キーワードを使います。

: register( [shader_profile], Type#[subcomponent])
shader_profileはつけるつけないは自由のオプションですが、"ps_5_0"等のシェーダープロファイルです。
Type#[subcomponent]は変数を関連付けるレジスターです。
Typeはレジスターのタイプで、subcomponentはレジスターの番号です。

以下にMSDNより表を載せます:
Type 対応するリソース
b 定数バッファ
t テクスチャとテクスチャバッファ
c Buffer offset
s Sampler
u Unordered Access View


これを使うと前述のコードはこうなります。
RWStructuredBuffer<int> BufferSum : register(u0);
RWStructuredBuffer<int> Buffer0 : register(u1);
RWStructuredBuffer<int> Buffer1 : register(u2);

これで3つの変数をUnorderedAccessViewのそれぞれ0番目、1番目、2番目として使います。
こうすると、3つの変数の配列中の位置は固定されます。
たとえ並び替えても

RWStructuredBuffer<int> Buffer0 : register(u1);
RWStructuredBuffer<int> Buffer1 : register(u2);
RWStructuredBuffer<int> BufferSum : register(u0);

これでも以前と同じように動作してくれます。


コード

コードはこのようになります。
なお、CPUからアクセスできるバッファの内部を初期化することに
意味はもはや無いので(実は前回からすでに意味はなかったのですが)
その部分のコードを削除しています。

Program.cs
using System.Collections.Generic;
using System.Linq;

using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.D3DCompiler;

class Program
{
    static Buffer bufferSum;
    static Buffer buffer0;
    static Buffer buffer1;

    static void Main(string[] args)
    {
        Device device = new Device(DriverType.Hardware);

        bufferSum = createStructuredBuffer(device, Enumerable.Range(0, 10).ToArray());
        buffer0 = createStructuredBuffer(device, Enumerable.Range(0, 10).ToArray());
        buffer1 = createStructuredBuffer(device, Enumerable.Range(0, 10).ToArray());

        initComputeShader(device);
        device.ImmediateContext.Dispatch(10, 1, 1);
        writeBuffer(device, bufferSum);
    }

    static void initComputeShader(Device device)
    {
        device.ImmediateContext.ComputeShader.SetUnorderedAccessViews(
            new[]
                {
                    new UnorderedAccessView(device, bufferSum),
                    new UnorderedAccessView(device, buffer0),
                    new UnorderedAccessView(device, buffer1),
                },
            0,
            3
            );

        ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
            "MyShader.fx",
            "MyComputeShader",
            "cs_5_0",
            ShaderFlags.None,
            EffectFlags.None
            );
        device.ImmediateContext.ComputeShader.Set(
            new ComputeShader(device, shaderBytecode)
            );
    }

    static void writeBuffer(Device device, Buffer buffer)
    {
        Buffer cpuAccessibleBuffer = createCpuAccessibleBuffer(device, buffer.Description.SizeInBytes);
        device.ImmediateContext.CopyResource(buffer, cpuAccessibleBuffer);
        int[] readBack = readBackFromGpu(cpuAccessibleBuffer);

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

    static Buffer createStructuredBuffer(Device device, int[] initialData)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                BindFlags = BindFlags.UnorderedAccess,
                OptionFlags = ResourceOptionFlags.StructuredBuffer,
                StructureByteStride = sizeof(int)
            }
            );
    }

    static Buffer createCpuAccessibleBuffer(Device device, int sizeInBytes)
    {
        return new Buffer(
            device,
            new BufferDescription
            {
                SizeInBytes = sizeInBytes,
                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;
    }
}



HLSL側のファイルはこうです。
変数として宣言しているバッファが増え
演算シェーダーの中でそれを使うように変わっています。

MyShader.fx
RWStructuredBuffer<int> BufferSum : register(u0);
RWStructuredBuffer<int> Buffer0 : register(u1);
RWStructuredBuffer<int> Buffer1 : register(u2);

[numthreads(1, 1, 1)]
void MyComputeShader(uint3 threadID : SV_DispatchThreadID )
{
    BufferSum[threadID.x] = Buffer0[threadID.x] + Buffer1[threadID.x];
}

実行結果

0
2
4
6
8
10
12
14
16
18

今回やっていることはこうです:
まず「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というバッファを3つ用意します。
 
そして2つのバッファの合計
「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」
                        +
「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」 
を残りの1つに書き込むのです。
すると残りの一つは
「0, 2, 4, 6, 8, 10, 12, 14, 16, 18」になります。
 
それをSystem.Console.WriteLine()で書きだしています。
ですから、このような2の倍数な値が表示されているわけですね



拍手[1回]


C#でGPGPUチュートリアル その3 演算シェーダー

ベクトルを2倍にする

 前回と前々回は、
GPUにあるデータをCPUに
持ってくるというのがテーマでした。

GPGPUのチュートリアルでありながら、
やっているのはデータの移動だけで
GPUに計算をさせていません。

今回こそ計算させてみましょう。

今回行う計算は、あるバッファの中身を2倍にするというものにします。

doubleTheContentsOfGpuBuffer.jpg

各要素にそれぞれ、同じ計算(2倍する)をするので、
並列処理っぽい、CPUよりもGPUが得意とする計算なはずです。


演算シェーダー

GPGPUな計算に使うシェーダーを
演算シェーダーといいます。
演算シェーダーはDirectX11で初めて導入された機能です。

SlimDXで演算シェーダーを使うには、
DeviceContextクラスのComputeShaderプロパティを通すことになります。
public ComputeShaderWrapper ComputeShader { get; }
SlimDX.Direct3D11.ComputeShaderWrapperクラスは
演算シェーダーの機能をまとめたクラスです。
演算シェーダーの実行に必要な設定用メソッドが用意されています。
(ただし、演算シェーダーの実行そのものにはこのクラスは関与しません。
実行にはDeviceContextクラスのメソッドを使います。後述)


計算結果を格納するバッファ

今回演算シェーダーの計算結果を格納するバッファを指定する方法は、
ComputeShaderWrapper.SetUnorderedAccessView()メソッドを使うというものです。
public void SetUnorderedAccessView(UnorderedAccessView unorderedAccessView, int slot);
unorderedAccessViewは演算シェーダーにセットするUnorderedAccessViewです。
slotはデバイスに存在するUnorderedAccessView配列のインデックスです。つまり配列のどこにセットするのかということですね。

バッファを指定するメソッドと言っておきながらこれの引数は
(バッファではなく)「なんとかかんとかView」です。
どういう事かというと、この「なんとかかんとかView」は
バッファをコンストラクタ引数に取るクラスで、
バッファの使われ方のようなものを表すのです。
バッファはデータの塊、Viewは機能の塊といったところでしょうか。

しかしなぜViewとバッファを別物として考えるのでしょうか?
DirectX9世代では少なくともViewというようなものはなかったはずです。
バッファに相当するものだけで何とかちゃんとやりくりできていたのです。
その疑問の答えは、
「現在のDirectXではリソースは一般的な形で存在し、色々な使い方ができるため、
その都度Viewで使い方を指定してやる必要がある」、
ということだと思います。(鵜呑みにしないでください)

たとえば、float2をテクスチャ座標としてSample()で色を獲得する一般的なテクスチャ
(DirectX9でピクセルシェーダー内で使っていたようなテクスチャ、Texture2Dです)
より厳密にuint2をインデックスとして色を獲得できるテクスチャ
(演算シェーダーの登場によってこういうテクスチャが現れました。RWTexture2Dです)
を考えてみましょう。
ある一つのテクスチャをある局面ではfloat2のテクスチャ、
別の局面ではuint2のテクスチャとして使いたくなった場合、
バッファとViewが同じものだった場合インターフェースがやや面倒なことになるかもしれません。
クラスの凝集度が下がりそうです。

一方、BufferとViewを別のものにしていた場合、
1つのバッファに複数のViewを割り当てる」ことで上記のような場合も柔軟に対応できるでしょう。
そういったわけで、データそのものを意味するBufferと、
そのデータに対する操作を表すViewに分離しているようです。
(概念的にはobjectを様々なインターフェースにキャストすることに似ているかもしれません。
こっちはあまり褒められたものではありませんが)

なお、3DCGのカメラのViewとはなんの関係もありません。

ですから、「なんとかかんとかViewを作る」と言った場合それが意味するのは
「このバッファはなんとかかんとかな役割を果たす」ということです。
ここではSlimDX.Direct3D11.UnorderedAccessViewですが、これは
「このバッファは一度に複数のスレッドからのランダムアクセスな読み書きが出来る」という意味です。
public class UnorderedAccessView : ResourceView

なぜバッファをUnorderedAccessViewとして扱う必要があるのでしょう?
それは今回のようなベクトルの各要素にアクセスするような並列計算をする場合、
配列の"[]"演算子を使うのが手っ取り早いからです。
UnorderedAccessViewでないということは、
C#で喩えるならT[]ではなくIEnumerable<T>を使うようなものです
(ただしUnorderedAccessViewの場合は書き込みの話です。
読み込みはUnorderedAccessViewでなくてもランダムアクセスっぽく[]でアクセスできるようです)。
IEnumerable<T>はそのままでは並列的な計算に向きませんよね。
Parallel.Forの中でアクセスするのはIEnumerable<T>の要素ではなくT[]の要素にしたいものです。

別の言い方をするなら、ピクセルシェーダーのように書込み先の位置が決まっているのではなく、
[]演算子で好きな位置に書き込めるということです。
ピクセルシェーダーでは計算した結果を格納する先は決まっていて、
プログラマが出来ることといったら書きこむデータを計算することだけでした。
UnorderedAccessViewなら計算した結果を自由な位置に書き込めます。

コンストラクタは4つ用意されていますが
今回使うのは一番簡単なこのコンストラクタです。
public UnorderedAccessView(Device device, Resource resource);
deviceはこのViewを持つデバイスです。
resourceはこのビューがその役割を指定するリソース(この場合はバッファ)です。

なお、~ViewクラスはSlimDXに4つ存在するようです。
~Viewクラス 説明
DepthStencilView 深度ステンシルテストでテクスチャリソースとして使います。
RenderTargetView レンダーターゲットなテクスチャーリソースとして使います。画面に映る描画先のことですね。
ShaderResourceView シェーダーリソースとして使います。たとえば定数バッファ、テクスチャバッファ、テクスチャ、サンプラーなどです。演算シェーダーでも計算に必要なデータを格納したバッファをこれに指定することがあります。
UnorderedAccessView ピクセルシェーダーか演算シェーダーで、複数のスレッドからのランダムアクセスっぽい読み書きを受け付ける、ということを意味します。


さて、生成したバッファをUnorderedAccessとして使う場合、
Bufferのコンストラクタで指定しなければならないオプションがあります。
コンストラクタに入れるBufferDescriptionのBindFlagsプロパティを、
以下のようにBindFlags.UnorderedAccessにしなくてはいけないのです。

BindFlags = BindFlags.UnorderedAccess

これは、生成するバッファを引数にUnorderedAccessViewを生成できるということです。

ちなみに、BindFlagsには他にも以下のようなオプションがあります。
BindFlagsというのはバッファの使われ方を意味する、
SlimDX.Direct3D11.BindFlags型のプロパティです。

BindFlagsの値 数値 説明
None 0 特に何も指定しません。
VertexBuffer 1 生成されるリソースが、Input Assemblerステージで、頂点バッファとして使えることを意味します。
IndexBuffer 2 生成されるリソースが、Input Assemblerステージで、インデックスバッファとして使えることを意味します。
ConstantBuffer 4 生成されるリソースが、シェーダーステージで、定数バッファとして使えることを意味します。
ShaderResource 8 生成されるリソースが、シェーダーステージで、バッファやテクスチャとして使えるとうことを意味します。
StreamOutput 16 生成されるリソースが、Stream Outputステージで、出力バッファとして使用できるということを意味します。Stream Outputステージとは頂点シェーダーとピクセルシェーダーステージの間に位置するステージの一つです。どうやらジオメトリーシェーダーとセットになっているらしく、新たな頂点データをメモリに書き出すことができます。
RenderTarget 32 生成されるリソースが、Output Mergerステージで、レンダーターゲットとして使えることを意味します。つまり普通のDirectXアプリケーションで画面に表示される画像のことですね。
DepthStencil 64 生成されるリソースが、Output Mergerステージで、深度ステンシルとして使えることを意味します。
UnorderedAccess 128 生成されるリソースが、ランダムアクセス風に、複数のスレッドから同時に読み書きできることを意味します。

数値が0,1,2,4…となっていることからわかるように、
これは複数のフラグを"|"で一緒に指定することができます。


演算シェーダーのセット

演算シェーダーで行う具体的な計算は、
C#ではなくHLSLのファイルに書きます。

C#側で行うのは、HLSLファイルをロードしてコンパイルしてセットすることです。

HLSLファイルをロードしてコンパイルするには、
ShaderBytecode.CompileFromFile()メソッドを使います。
このメソッドには6種類オーバーロードがあるのですが、
今回使うのは2番目に簡単なこのメソッドです。
(といっても引数が結構多いです。
思うにこれはC#の名前付き引数機能を使えば簡単になりそうですね)

public static ShaderBytecode CompileFromFile(
    string fileName, 
    string entryPoint, 
    string profile, 
    ShaderFlags shaderFlags, 
    EffectFlags effectFlags
    );
これはDirectX SDK付属のHLSLコンパイルツール、fxc.exeに似ています。

fileNameはシェーダー(今回の場合は演算シェーダー)を記述したHLSLファイルの名前。
entryPointはHLSLファイルの中の、シェーダーとして使う関数の名前。
profileはシェーダーのターゲットです。演算シェーダーなら"cs_4_0"とか"cs_5_0"などというふうな文字列です。頂点シェーダーなら"vs_5_0"、ピクセルシェーダーなら"ps_5_0"という感じになるでしょう。
shaderFlagsはシェーダーコンパイルオプションです。今回はとくにどうでもいいのでNoneを選びます。
effectFlagsはエフェクトコンパイルオプションです。今回は特にどうでもいいのでNoneを選びます。

こうしてコンパイルされると、戻り値としてShaderBytecodeが帰ってきます。
このShaderBytecodeを使って
SlimDX.Direct3D11.ComputeShaderクラスのインスタンスを生成します。

public class ComputeShader : DeviceChild

public ComputeShader(Device device, ShaderBytecode shaderBytecode);

deviceは演算シェーダーを作るデバイス。
shaderBytecodeは演算シェーダーのバイトコードです。

このコンストラクタを使ってできたComputeShaderを
実行出来るようセットする必要があります。
それにはComputeShaderWrapper.Set()メソッドを使います。

public void Set(ComputeShader shader);
shaderはデバイスにセットする演算シェーダーです。

演算シェーダーの実行

演算シェーダーに関するセッティングが全て終わったら、
いよいよ演算シェーダーを実行できます。
実行にはComputeShaderWrapper.Dispatch()メソッドを使います。

public void Dispatch(
    int threadGroupCountX, 
    int threadGroupCountY, 
    int threadGroupCountZ
    );

さてここには3つの引数がありますがなんでしょうか?
それを説明するにはまず演算シェーダーのスレッドの簡単な説明が必要です。

演算シェーダーは多数のスレッドを同時に実行できます。

gpgpuThreads3D.png

そしてそれらのスレッドはグループ分けされているのです。

gpgpuThreadsGrouped.png
Dispatch()メソッドの引数はこのグループの数を指定するのです。
(全スレッドの数ではありません。念のため)


numthreads属性 (HLSL側)

では1グループあたりのスレッドの数はどうやって指定するのでしょうか?
それにはHLSL側のnumthreads属性を使います。

numthreads(X, Y, Z)

X, Y, Zは1グループあたりのスレッドサイズ(?いい表現が思いつきませんでした)です。
1グループあたりのスレッドの数はX * Y * Z個になります。
これは全スレッドの数を指定するのではないことに注意してください。
あくまでも1グループにいくつスレッドがあるかを指定するものです。
全スレッドの数はC#側のDispatchの引数とこのX, Y, Zをかけ合わせたもので決まります。

なお、X, Y, Zには範囲に制限があります。
MSDNの表をそのまま持ってきます:

演算シェーダーバージョン Zの最大値 スレッドの最大個数(X * Y * Z)
cs_4_x 1 768
cs_5_0 64 1024


SV_DispatchThreadID (HLSL側)

さて、このように演算シェーダーでは
多数のスレッドで計算することができます。
こういうことをしていると、
「今演算シェーダーの関数がどのスレッドで実行されているのか」
ということが知りたくなることが良くあります
たとえば今回行う「ベクトルのすべての要素を2倍にする」というようなものだと、
要素一つ一つにそれぞれスレッドを1つ割り当てることになりますが、
どの要素を操作するのかというキーとしてスレッドのIDを知りたくなるのです。

それにはSV_DispatchThreadIDセマンティックを使います。
こいつを演算シェーダーのuint3型の引数に付けてやると、それに
現在実行しているスレッドのIDが入っているのです。


RWStructuredBuffer<T> (HLSL側)

今回HLSLでの計算結果の読み書きはHLSLの
RWStructureBuffer<T>の変数に対して行います。
この型は、読み書きが出来る上(RWとはRead/Writeのことです)、
C#のジェネリクスのついたコレクションのように、
Tの中身を色々な構造体に指定することができます。
例えば今回の場合はRWStructureBuffer<int>というような具合です。

この型には(C#noジェネリクスのついたコレクションのように)
[]演算子も付いています。
T Operator[]( in uint indexPosition );

これは、引数にインデックスをとって、その位置の要素を返します。


コード

今回のコードは2つのファイルからなります。
C#のコードと、HLSLのコードです。

HLSLのコードは
Visual C#のプロパティ「Copy to Output Directory」を
「Copy if newer」にしましょう。

Program.cs
using System.Collections.Generic;
using System.Linq;

using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.D3DCompiler;

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

        initComputeShader(device, onlyGpuBuffer);
        device.ImmediateContext.Dispatch(10, 1, 1);
        writeBuffer(device, onlyGpuBuffer);
    }

    static void initComputeShader(Device device, Buffer onlyGpuBuffer)
    {
        device.ImmediateContext.ComputeShader.SetUnorderedAccessView(new UnorderedAccessView(device, onlyGpuBuffer), 0);

        ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
            "MyShader.fx",
            "MyComputeShader",
            "cs_4_0",
            ShaderFlags.None,
            EffectFlags.None
            );
        device.ImmediateContext.ComputeShader.Set(
            new ComputeShader(device, shaderBytecode)
            );
    }

    static void writeBuffer(Device device, Buffer buffer)
    {
        Buffer cpuAccessibleBuffer = createCpuAccessibleBuffer(device, Enumerable.Range(0, 10).ToArray());
        device.ImmediateContext.CopyResource(buffer, cpuAccessibleBuffer);
        int[] readBack = readBackFromGpu(cpuAccessibleBuffer);

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

    static Buffer createStructuredBuffer(Device device, int[] initialData)
    {
        DataStream initialDataStream = new DataStream(initialData, true, true);
        return new Buffer(
            device,
            initialDataStream,
            new BufferDescription
            {
                SizeInBytes = (int)initialDataStream.Length,
                BindFlags = BindFlags.UnorderedAccess,
                OptionFlags = ResourceOptionFlags.StructuredBuffer,
                StructureByteStride = sizeof(int)
            }
            );
    }

    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;
    }
}


MyShader.fx
RWStructuredBuffer<int> MyBuffer;

[numthreads(1, 1, 1)]
void MyComputeShader( uint3 threadID : SV_DispatchThreadID )
{
    MyBuffer[threadID.x] *= 2;
}



実行結果
0
2
4
6
8
10
12
14
16
18

2倍になりました!

このプログラムがしていることは
GPU中に「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」というStructuredBufferをつくり、
演算シェーダーでそれぞれ2倍し「0, 2, 4, 6, 8, 10, 12, 14, 16, 18」にした、ということです。


10個のスレッドで、演算シェーダーの関数
MyComputeShader()を実行しているのです。
(10個のスレッドグループを作り、
1つのスレッドグループに1つのスレッドがあるので全部で合計10個のスレッドです)

MyComputeShaderの引数threadIDの中には、
(0, 0, 0)
(1, 0, 0)
(2, 0, 0)
(3, 0, 0)
(4, 0, 0)
(5, 0, 0)
(6, 0, 0)
(7, 0, 0)
(8, 0, 0)
(9, 0, 0)
のいずれかが入っているはずです。

そしてのXの値をインデックスにして、
バッファにアクセスし数を二倍にしています。
つまりバッファの中の全てのintは2倍されるのです。


拍手[1回]