忍者ブログ

Memeplexes

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

[PR]

×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。


C#でDirectX11 SlimDXチュートリアルその14 定数バッファー

定数バッファー

ずいぶん前回と間があきましたが、SlimDXについてメモしておこうと思います。
今回は、定数バッファーです。

定数バッファーとは、CPUからGPUへのデータ転送をまとめるデータの固まりです。
どういうことでしょう?
3DCGを描くときのことを考えてください。
CPUからGPUへ、モデルの回転やライトの位置など、情報を転送しなければいけません。
そしてその転送にはオーバーヘッドがかかります。
これをいちいち別々に転送していては、オーバーヘッドが膨らむばかり。
そこでこれらを一つにまとめて一気にGPUへ転送しようということです。
そのひとつにまとめられたものを、コンスタントバッファーといいます。


定数バッファーの生成

定数バッファーは、Bufferクラスのインスタンスです。
頂点バッファやインデックスバッファと同じですね。
ただ使い方が違うのです。
そして初期化の方法も。
初期化するときには、BindFlagsにBindFlags.ConstantBufferを指定します。

    private void initConstantBuffer()
    {
        constantBuffer = new Buffer(
            GraphicsDevice,
            new BufferDescription
            {   
                //なぜかsizeof(float) * 4以上でないと例外をスローする。
                SizeInBytes = sizeof(float) * 4,  
                BindFlags = BindFlags.ConstantBuffer,
            }
            ); 
    }

ここで気をつけて欲しいのは、SizeInBytesにsizeof(float) * 4より小さな値を代入するとマズイ事になるかもしれないということです。
大してデータを使わないからと言って、もし小さな値を代入すれば、Direct3D11Exceptionをスローすることがあるので注意してください。
少なくとも私の環境ではそうでした。
すべての環境でそうであるかはわかりません。
しかし、Vector4より小さな値が禁止されているというのはおかしな話ではないでしょう。


定数バッファーの更新(CPUからGPUへのデータ転送)

定数バッファーはCPUからGPUへのデータ転送をまとめるためのバッファです。
するとデータ転送をする方法があるはずなのですが、Bufferそのものにはそんなメソッドはありません。
データ転送には、DeviceContext.UpdateSubresourceメソッドを使います。

public void UpdateSubresource(DataBox source, Resource resource, int subresource);

sourceは、GPUに転送するデータです。
resourceは転送先のGPUに存在するリソース。
subresourceはサブリソース番号です。


定数バッファをエフェクトにセット

更新した定数バッファも、少なくとも一度はエフェクトにセットしなければ描画に反映されません。
定数バッファのエフェクトへのセットには次のようにします:
 
        effect.GetConstantBufferByName("myConstantBuffer").ConstantBuffer = constantBuffer;

effectはセットするエフェクト。
"myConstantbuffer"は、HLSLファイル中の定数バッファの名前です。
constantBufferはセットする定数バッファです。


サンプルコード

Program.cs
using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.DXGI;
using SlimDX.D3DCompiler;

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

struct MyConstantBuffer
{
    public float OffsetX;
    public float OffsetY;
}

class MyGame : Game
{
    Effect effect;
    InputLayout vertexLayout;
    Buffer vertexBuffer;
    Buffer constantBuffer;

    protected override void Draw()
    {
        GraphicsDevice.ImmediateContext.ClearRenderTargetView(
            RenderTarget,
            new Color4(1, 0, 0, 1)
            );

        updateConstantBuffer();
        initTriangleInputAssembler();
        drawTriangle();
        
        SwapChain.Present(0, PresentFlags.None);
    }

    private void updateConstantBuffer()
    {
        double time = System.Environment.TickCount / 500d;
        MyConstantBuffer myConstantBuffer = new MyConstantBuffer
        {
            OffsetX = (float)System.Math.Sin(time) * 0.4f,
            OffsetY = (float)System.Math.Sin(time) * 0.8f
        };
        GraphicsDevice.ImmediateContext.UpdateSubresource(
            new DataBox(0, 0, new DataStream(new[] { myConstantBuffer }, true, true)),
            constantBuffer,
            0
            );
        effect.GetConstantBufferByName("myConstantBuffer").ConstantBuffer = constantBuffer;
    }

    private void initTriangleInputAssembler()
    {
        GraphicsDevice.ImmediateContext.InputAssembler.InputLayout = vertexLayout;
        GraphicsDevice.ImmediateContext.InputAssembler.SetVertexBuffers(
            0,
            new VertexBufferBinding(vertexBuffer, sizeof(float) * 3, 0)
            );
        GraphicsDevice.ImmediateContext.InputAssembler.PrimitiveTopology
            = PrimitiveTopology.TriangleList;
    }

    private void drawTriangle()
    {
        effect.GetTechniqueByIndex(0).GetPassByIndex(0).Apply(GraphicsDevice.ImmediateContext);
        GraphicsDevice.ImmediateContext.Draw(3, 0);
    }


    protected override void LoadContent()
    {
        initEffect();
        initVertexLayout();
        initVertexBuffer();
        initConstantBuffer();
    }

    private void initEffect()
    {
        using (ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
            "myEffect.fx", "fx_5_0",
            ShaderFlags.None,
            EffectFlags.None
            ))
        {
            effect = new Effect(GraphicsDevice, shaderBytecode);
        }
    }

    private void initVertexLayout()
    {
        vertexLayout = new InputLayout(
            GraphicsDevice,
            effect.GetTechniqueByIndex(0).GetPassByIndex(0).Description.Signature,
            new[] { 
                    new InputElement
                    {
                        SemanticName = "SV_Position",
                        Format = Format.R32G32B32_Float
                    }
                }
            );
    }

    private void initVertexBuffer()
    {
        vertexBuffer = MyDirectXHelper.CreateVertexBuffer(
            GraphicsDevice,
            new[] {
                new SlimDX.Vector3(0, 0.5f, 0),
                new SlimDX.Vector3(0.5f, 0, 0),
                new SlimDX.Vector3(-0.5f, 0, 0),
            });
    }

    private void initConstantBuffer()
    {
        constantBuffer = new Buffer(
            GraphicsDevice,
            new BufferDescription
            {   
                //なぜかsizeof(float) * 4以上でないと例外をスローする。
                SizeInBytes = sizeof(float) * 4,  
                BindFlags = BindFlags.ConstantBuffer,
            }
            ); 
    }

    protected override void UnloadContent()
    {
        effect.Dispose();
        vertexLayout.Dispose();
        vertexBuffer.Dispose();
        constantBuffer.Dispose();
    }
}

class Game : System.Windows.Forms.Form
{
    public SlimDX.Direct3D11.Device GraphicsDevice;
    public SwapChain SwapChain;
    public RenderTargetView RenderTarget;


    public void Run()
    {
        initDevice();
        SlimDX.Windows.MessagePump.Run(this, Draw);
        disposeDevice();
    }

    private void initDevice()
    {
        MyDirectXHelper.CreateDeviceAndSwapChain(
            this, out GraphicsDevice, out SwapChain
            );

        initRenderTarget();
        initViewport();

        LoadContent();
    }

    private void initRenderTarget()
    {
        using (Texture2D backBuffer
            = SlimDX.Direct3D11.Resource.FromSwapChain<Texture2D>(SwapChain, 0)
            )
        {
            RenderTarget = new RenderTargetView(GraphicsDevice, backBuffer);
            GraphicsDevice.ImmediateContext.OutputMerger.SetTargets(RenderTarget);
        }
    }

    private void initViewport()
    {
        GraphicsDevice.ImmediateContext.Rasterizer.SetViewports(
            new Viewport
            {
                Width = ClientSize.Width,
                Height = ClientSize.Height,
            }
            );
    }

    private void disposeDevice()
    {
        UnloadContent();
        RenderTarget.Dispose();
        GraphicsDevice.Dispose();
        SwapChain.Dispose();
    }

    protected virtual void Draw() { }
    protected virtual void LoadContent() { }
    protected virtual void UnloadContent() { }
}

class MyDirectXHelper
{
    public static void CreateDeviceAndSwapChain(
        System.Windows.Forms.Form form,
        out SlimDX.Direct3D11.Device device,
        out SlimDX.DXGI.SwapChain swapChain
        )
    {
        SlimDX.Direct3D11.Device.CreateWithSwapChain(
            DriverType.Hardware,
            DeviceCreationFlags.None,
            new SwapChainDescription
            {
                BufferCount = 1,
                OutputHandle = form.Handle,
                IsWindowed = true,
                SampleDescription = new SampleDescription
                {
                    Count = 1,
                    Quality = 0
                },
                ModeDescription = new ModeDescription
                {
                    Width = form.ClientSize.Width,
                    Height = form.ClientSize.Height,
                    RefreshRate = new SlimDX.Rational(60, 1),
                    Format = Format.R8G8B8A8_UNorm
                },
                Usage = Usage.RenderTargetOutput
            },
            out device,
            out swapChain
            );
    }

    public static Buffer CreateVertexBuffer(
        SlimDX.Direct3D11.Device graphicsDevice,
        System.Array vertices
        )
    {
        using (SlimDX.DataStream vertexStream 
            = new SlimDX.DataStream(vertices, true, true))
        {
            return new Buffer(
                graphicsDevice,
                vertexStream,
                new BufferDescription
                {
                    SizeInBytes= (int)vertexStream.Length,
                    BindFlags = BindFlags.VertexBuffer,
                }
                );
        }
    }
}
 
 

myEffect.fx
cbuffer myConstantBuffer
{
float OffsetX;
float OffsetY;
}

float4 MyVertexShader(float4 position : SV_Position) : SV_Position
{
    return position + float4(OffsetX, OffsetY, 0, 0);
}

float4 MyPixelShader() : SV_Target
{
    return float4(1, 1, 1, 1);
}

technique10 MyTechnique
{
pass MyPass
{
SetVertexShader( CompileShader( vs_5_0, MyVertexShader() ) );
SetPixelShader( CompileShader( ps_5_0, MyPixelShader() ) );
}
}

このプログラムは、三角形を斜めに平行移動します。
 
Tutorial14ConstantBufferTranslatingTriangle01.png
Tutorial14ConstantBufferTranslatingTriangle02.png

つまり定数バッファなど使わなくても出来るプログラムです。
というか大抵のことは定数バッファなど使わなくても出来るのです。

大切なのはパフォーマンスです。
変更点があるとすれば内部です。

このプログラムでは、三角形の平行移動に使うパラメーターを、定数バッファでまとめているのです。
平行移動に使うパラメーターは2つ有ります。
X方向のオフセットを表すOffsetX(float)と、Y方向のオフセットを表すOffsetY(float)です。
もし定数バッファを使わなければ、2回、CPUからGPUにデータを転送することになります。
しかしここでは定数バッファを使ってこの2つを一つにまとめているので、CPUからGPUへのデータ転送は、毎フレーム一回だけですんでいます。









拍手[1回]

PR

v4.0でv2.0のアセンブリを使うとFileLoadExceptionがスローされる問題 (Mixed mode assembly is built against version 'v2.0.50727' of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.)

このブログでも何度か取り上げましたが、独立して記事にしておきます。
VS2010などで、ランタイムバージョンが2.0のアセンブリを参照して使うと、次のような例外が出ます。

FileLoadException.PNG


FileLoadExceptionです。
ようは、アセンブリのバージョンが違うのでだめですよと言っているのです。

この問題を解決するには、.configファイルを使います。
バージョン2も使いますよと教えてあげるのです。

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>
</configuration>

このファイルを作って、プログファムを実行するとさっきのような例外はスローされなくなるはずです。



SlimDXでは

さて、私がこの問題に直面したのはSlimDXを使っている時でした。
SlimDXとはDirectXのC#用ラッパーライブラリーです。

このSlimDXのアセンブリを参照に追加しようとしている時です。
どうも一見SlimDXにはランタイムバージョン4.0のものが存在しないように見えたのです。
あたかも2.0のアセンブリだけであるかのように。

IsSlimDXOnlySupportV2.0.png

クリックして上の画像を拡大してみてください。
SlimDXにはランタイムバージョンが2.0しか無いように、一見見えます。
ですから私はそのままこのアセンブリをv4.0のプロジェクトに追加して、FileLoadExceptionに悩まされました。
冒頭のようにApp.configを追加しなければならなかったのです。

そんなわけで「なんでSlimDXはv4.0対応していないのだろう?不親切だなー。これさえなければいいライブラリなのに・・・」と思ったものです。
が、違うのです。
v4.0のものもちゃんと有ります。

思い出すだけで恥ずかしいのですが、これはRuntimeというところをクリックすると、v4.0のアセンブリが出てくるのです。
v4.0のものが先頭に出てきてからSlimDXで検索すると、きちんと出てきます。

SlimDXAlsoSupportsV4.0.png

ですから、実はSlimDXを使うときにApp.configファイルを作る必要など全くなかったのです。
うーん反省です。












拍手[2回]


Windows7 vs Windows XP(アクセス数~普及率)

 ぼーっとアクセスを眺めていて気がついたのですが、最近このブログへのユニークアクセス数で、Windows7がXPを抜き、1位になりました。
他のところではどうだかわかりませんがともかく、
祝!世代交代!!

AccessCountOfOSs.png

おわかりいただけたでしょうか。
このグラフはこのブログへアクセスしたPCをOS別に分類し、その推移を描いています。
アクセス数は普及率を反映していると思われ、このグラフはそのままOSの普及率として読み替えてもいいでしょう。

このとおり、3月まではXPが一位ですが、4月からここ3ヶ月はずっと7が一位です!
つまりXPの時代は終わり、7の時代が幕を開けたのです!!!

……
いや……
どうでしょうね。
記録がないのでわかりませんが、2月がどうだったのか。
2月がもし7が一位だったとすれば、世代交代はもうずっと前に起こっていた、あるいは今はまだ起こっている最中だと考えることもできそうです。

しかしまあ数だけ見れば今Windows 7が一位だということは確かです!
DirectXの対応状況を考えれば、これはとても好ましいこと。
現在の最新バージョンはDirectX11だと言うのに、XPはDirectX9までにしか対応していませんからね。
7が一位を取ったということは、それだけDirectX10以降の対応PCの数が増えている。
つまり、グラフィックスの進歩と前進につながるはずです・・・!!!!

ただ、7が一位を取ったというのには、おそらくこのブログでDirectX10や11を扱ったというのも、関係があるかもしれません。
つまりデータにバイアスがかかっているのです。
DirectX10以降を実行できないPCでは、このブログに書かれていることは、あまり意味が無いかもしれないですからね。
うーん・・・

拍手[1回]


[CUDA] メモリーコアレッシング(Memory coalescing)

 CUDAの話です。
先日CUDAのコードを読んでいると、不可解なコードに行き当たりました。

energygrid[outaddr] += energyvalx1;
energygrid[outaddr + 1 * BLOCKSIZEX] += energyvalx2;
energygrid[outaddr + 2 * BLOCKSIZEX] += energyvalx3;
...
energygrid[outaddr + 6 * BLOCKSIZEX] += energyvalx7;
energygrid[outaddr + 7 * BLOCKSIZEX] += energyvalx8;

これは配列energygrid[]に、floatの値を足しているようです。
それはいいのですが、インデックスに注目してください。
1 * BLOCKSIZEXとはなんでしょう?
2 * BLOCKSIZEXとは?

調べてみると、BLOCKSIZEXは16でした。
つまり言い換えると次のようにアクセスしていることになります。

energygrid[outaddr] += energyvalx1;
energygrid[outaddr + 16] += energyvalx2;
energygrid[outaddr + 32] += energyvalx3;
...
energygrid[outaddr + 96] += energyvalx7;
energygrid[outaddr + 112] += energyvalx8;

……
ちょっと待ってください。
なぜ16飛び飛びにアクセスしているのでしょうか?
このコードの元の目的から考えると(ここでは解説しませんが)、プログラムはenergygridすべての要素にアクセスします。
というか全要素にアクセスしなければなりません。
全要素にアクセスするなら飛び飛びアクセスでなくてもいいんじゃ!?
単に順番に、[outaddr]、[outaddr + 1]、[outaddr + 2]、...[outaddr + 7]というふうにアクセスしてはなぜいけないのでしょう?

しばらく頭を捻った後ようやくわかりました。
つまりこれは複数のスレッドで考えると話が見えてくるのです。
今までは一つのスレッドでこう考えていました:

cudaMemoryCoalescingThread0.JPG

こう考えるとたしかに不可解なのです。
インデックスが16飛び飛びですから。
「どうして0,1,2,3じゃなくて0, 16, 32, 48なの?」となるわけです。

しかしCUDAはGPGPU。
スレッドは多数用意して計算するもの。
そうすると実行時には次のようになります:

cudaMemoryCoalescingThreads16.JPG

おわかりいただけたでしょうか…
一つのスレッドで考えるとたしかに飛び飛びだったものが、複数スレッドで見るとアクセスが綺麗に連続していることがわかります。
全部のスレッドの最初のアクセスは、0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15と連続しています。
そのつぎもやはり連続しています。

CUDAではこのように複数のスレッドの固まりが連続したところにアクセスしていると、そのメモリアクセスをまとめ(Coalescing)てくれるそうなのです。
こうしてまとめられたメモリアクセスは、そうでないものより効率がいいそうです。
だからわざわざ*16していたのですね。

ちなみに16というのは、半ワープというもののサイズです。
CUDAでは実行するスレッドはワープという固まりをなします。
それが32個です。
1ワープ32スレッド。
半ワープというのはその半分の16個です。
メモリアクセスのまとめ(Coalescing)は、半ワープが単位となっているそうです。

なお、GPU使っているのにスレッドが16個しか出来ないの?と思われる方もいらっしゃるでしょうが、
実際にはそれより多いです。
上のプログラムでは16より多い数のスレッドを起動していました。
スレッド16 ~ 31やそれ以降は、また別のまとめ(Coalescing)をしているということです。

拍手[5回]


C#でOpenCL入門 チュートリアル一覧

 C#でOpenCLを使うシリーズの一覧をまとめます。
まだメモリフェンスだとか重要な機能を説明していないような気もしますが、どうもいじってみたところ、使わなくてもそれなりのことが出来るような気がします。
(メモリフェンスがなければ失敗するようなプログラムを書こうとしたのですが、仕様にない動作?により上手く動いてしまいました。環境によるのかもしれませんんが)

というわけでここで一区切り。
全部で12の記事になりました:

  1. プラットフォーム
  2. デバイス
  3. コンテキスト
  4. コマンドキュー
  5. バッファ
  6. プログラム
  7. カーネル
  8. データ並列
  9. コンパイルエラー捕捉
  10. グループID、ローカルID
  11. 非バッファ引数
  12. スレッドとグループの個数

感想

以前、似たような技術であるDirectX11のコンピュートシェーダーを扱いました。
それと比べるとOpenCLは簡単かつ柔軟な印象を受けました。

まずインターフェースが綺麗です。
DirectXではなんだか初期化が面倒くさいですがOpenCLはそんなことはありません。
まぁ「そんなことはありません」というと正確ではないかもしれませんが、グラフィックスは無視してGPGPUだけを考えているためか、ましなほうです。
また、スレッドグループのスレッド個数もDirectX11ではHLSLにハードコーディングしなくてはいけませんが、OpenCLではCPU側から柔軟に変更できます(意味があるかはあまりわかりませんが、柔軟といえば柔軟です)。

というふうにOpenCLのいいところだけ書きましたが、APIがC言語で書かれているので、C#との相互運用がちょっとめんどくさいですね。
誰かC#用OpenCLラッパーライブラリを書いてくれればだいぶ使いやすくなりそうです。

APIもラッパーを書きやすいタイプな気がします。
Managed Wrapper for OpenCLなんてのが出たらいいんですけどね。

拍手[1回]