忍者ブログ

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