忍者ブログ

Memeplexes

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

C#でGPGPUチュートリアル その2バッファのコピー

バッファからバッファへコピー

前回
、GPUにあるデータをCPU側に持ってきました。
図で表すと以下のようなことをしたのです。

howTutorial01Worked.jpg



しかし実際にGPGPUをやろうとすると、
これだけでは計算結果をCPU側に持ってこれないことがわかります。

どうやら「CPUからアクセス可能なバッファ」は計算結果を入れられないようなのです。
計算結果を入れることが可能であり、
かつCPUからアクセスできるバッファをつくろうとすると例外をスローします。

ではどうすればいいのか?
と言いますと、間にひとつバッファをはさんでやればいいのです。

howSlimDXComputeShaderTutorial02Works.jpg

「計算結果を入れることができるけれどCPUからはアクセス出来ないバッファ」
に計算結果を入れます。
そこから
「CPUからアクセスはできるけど計算結果を入れることができないバッファ」
にデータをコピーしてやるのです。
そうすれば後は前回と同じようにCPU側に持ってこれます。

ですから、今回しなくてはならないのはバッファからバッファへのコピーです。
それとついでに、「計算結果を入れることができるけれどCPUからはアクセス出来ないバッファ」の生成ですね。

バッファからバッファへのコピーには
DeviceContext.CopyResource()メソッドを使います。
public void CopyResource(Resource source, Resource destination);
これは珍しく簡単なメソッドで、解説の必要は無いかもしれません。
ですが念のため言っておくと、
sourceはコピー元で、
destinationがコピー先です。


計算結果を入れるバッファの生成

計算結果を入れるバッファは
StructuredBufferというものにしてやります。
StructuredBufferとは何かというとGPU側の配列のようなものです。
つまり、ある一定のサイズの構造体がズラッと列になって並んでいるようなものです。

では「そうでないバッファなどあるのだろうか、
頂点バッファは頂点のサイズは一定だし、
インデックスバッファはインデックスのサイズが一定で
ズラッとならんでいるのに」
と思われるかもしれません。

ですが、実はDirectX11にはそういうものがあって、
たとえば定数バッファです。
定数バッファの中身はひとつのマトリックスとひとつのベクトルという可能性がありえます。
(Projectionマトリックスとライトの向きとかでしょうか)
マトリックスとベクトルは当然サイズが違います。
つまり定数バッファは要素のサイズが一定でない、
言い換えるとStructuredBufferではない、ということなのでしょうね。(要素数が1でない限り)

生成時にStructuredBufferにしてなんの得があるのか、ということですが、
HLSL側で配列のようにカッコ"[ ]"を使ってアクセスすることができます。
StructuredBufferを使わない場合は(それは可能です)、
"[ ]"ではなく「バッファの~バイト目にアクセスしそれをintと解釈して読み込む」
というような感じの面倒な使い方をしなくてはなりません。
ちなみにHLSLのコードでは変数の型としてStructuredBuffer<int>というような書き方をします。
(より正確にはStructuredBuffer<構造体名>です。
そうすると配列というよりはジェネリクスのコレクションっぽい物といった感じでしょうか)

C#側ではSlimDXにStructuredBufferという型そのものがあるわけでは無いようで、
普通のBufferクラスのコンストラクタ引数BufferDescriptionの内容を
OptionFlags = ResourceOptionFlags.StructuredBuffer,
StructureByteStride = 配列要素一つのサイズ
としてやることでStructuredBufferにすることができます。

 OptionFlagsはバッファのその他の設定を表す、
SlimDX.Direct3D11.ResourceOptionFlags型のプロパティです。

ResourceOptionFlagsの値 数値 説明
None 0 オプションの指定はなしです。
GenerateMipMaps 1 生成するリソースががテクスチャの場合、mipmapの生成を有効にします。これを使う場合、BindFlagsでこのリソースがレンダーターゲットであり、シェーダーリソースであるということを指定しなければいけません。
Shared 2 生成するリソースが複数のDirect3Dデバイス間でのデータ共有することを可能にします。ただし2Dのテクスチャでなくてはいけません。
TextureCube 4 生成するリソースが6枚のテクスチャから成る、テクスチャキューブになることを可能にします。
DrawIndirect 16 GPUで生成されたコンテントのインスタンシングを可能にします。たぶんインスタンシングを使って描画するので、直接描画するわけではない、ということなのでしょう。
RawBuffer 32 生成するリソースが、ひとかたまりの、バイトアドレスで操作するバッファでになることを可能にします。
StructuredBuffer 64 生成するリソースが、Structured Bufferとなることを可能にします。たくさんの構造体が並んだ配列のようなものです。
ClampedResource 128 DeviceContext.SetMinimumLod()メソッドを使ったときに、mipmapのclampを有効にします。
KeyedMutex 256 SlimDX.DXGI.KeyedMutexクラスの、KeyedMutex.Acquire()メソッドとKeydMutex.Release()メソッドを使ったときに、シンクロするようになります。
GdiCompatible 512 リソースがGDIと互換性を持てます。これを指定するとSlimDX.Direct2D.GdiInteropRenderTargetクラスのGetDC()メソッドを使ってGDIで描画できます。

今回は、上記のうちStructuredBufferだけをオンにします。

さて、実はこの設定をしただけでは、まだCPUからアクセス出来るのです。
GPUで計算結果を格納できるような設定にすると
例外をスローするようになるのですが、
その設定については次回にしたいと思います。

コード

GPUのバッファからバッファへコピーして、
それをCPUに持ってくるプログラムは以下のようになります。

Program.cs
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 onlyGpuBuffer = createStructuredBuffer(device, Enumerable.Range(10, 10).ToArray());
        writeBuffer(device, onlyGpuBuffer);
    }


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

このプログラムはまず、
GPU内でだけアクセスできるバッファ「10, 11, 12, 13, 14, 15, 16, 17, 18, 19」
を作ります。

次にGPUに、CPUからもアクセスできるバッファ「0, 1, 2, 3, 4, 5, 6, 7, 8, 9」
を作ります。

前者から後者に値をコピーします。
すると後者の値は消えて、後者の方も「10, 11, 12, 13, 14, 15, 16, 17, 18, 19」
になります。

それをCPUに読み込みます。
その結果を出力するので、
このプログラムは実行すると以下のようになります。
10
11
12
13
14
15
16
17
18
19

ここで表示されているのは
もとはGPU内からしかアクセス出来ないバッファに
格納されていたint配列です。

それが回りまわってきちんとCPU側に引き渡され
System.Console.WriteLine()で出力できました。










拍手[0回]

PR