Windows と C++

デスクトップ アプリケーションでの Direct2D によるレンダリング

Kenny Kerr

 

Kenny Kerr前回のコラムでは、ライブラリやフレームワークをまったく使わないで、実際、いかに簡単にデスクトップ アプリケーションを作成できるかを紹介しました。事実、後の苦労を厭わなければ、図 1 のように、WinMain 関数にデスクトップ アプリケーション全体を収めることもできます。もちろん、このようなアプローチは拡張が難しくなります。

 

 

図 1 拡張性のないウィンドウ

int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int)
{
  WNDCLASS wc = {};
  wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
  wc.hInstance = module;
  wc.lpszClassName = L"window";
  wc.lpfnWndProc = [] (HWND window, UINT message, WPARAM
    wparam, LPARAM lparam) -> LRESULT
  {
    if (WM_DESTROY == message)
    {
      PostQuitMessage(0);
      return 0;
    }
    return DefWindowProc(window, message, wparam, lparam);
  };
  RegisterClass(&wc);
  CreateWindow(wc.lpszClassName, L"Awesome?!",
    WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr);
  MSG message;
  BOOL result;
  while (result = GetMessage(&message, 0, 0, 0))
  {
    if (-1 != result) DispatchMessage(&message);
  }
}

また、こうしたメカニズムの多くを省略するために Active Template Library (ATL) が提供する C++ の優れた抽象化手法と、Windows Template Library (WTL) によってこの抽象化をさらに進める手法も紹介しました。これらは主に、アプリケーション開発で USER アプローチや GDI アプローチに注力するアプリケーション向けの手法です (詳細については 2 月号のコラム msdn.microsoft.com/magazine/jj891018 (英語) を参照してください)。

Windows におけるアプリケーション レンダリングは、ハードウェア アクセラレータを使用する Direct3D に向かっています。しかし、2 次元アプリケーションやゲームをレンダリングするだけならば、実のところ、Direct3D を直接使用するのは実用的ではありません。こういう場合に使用するのは Direct2D です。数年前に Direct2D が初めて発表されたとき簡単に紹介しましたが、今後数か月をかけて、Direct2D 開発について詳しく見ていこうと思います。Direct2D のアーキテクチャと基礎の概要については、2009 年 6 月号のコラム「Direct2D の紹介」 (msdn.microsoft.com/magazine/dd861344) を参照してください。

Direct2D の重要な設計基盤の 1 つはレンダリングに重点が置かれていることで、Windows アプリケーション開発の他の側面は、開発者や開発者が使用できる他のライブラリに委ねられます。Direct2D はデスクトップ ウィンドウでレンダリングを行うために設計されていますが、実際にデスクトップ ウィンドウを用意して、Direct2D レンダリング向けに最適化するかどうかは、開発者の自由です。そこで今月は、Direct2D とデスクトップ アプリケーション ウィンドウの独特な関係に注目しようと思います。ウィンドウの処理やレンダリングのプロセスを最適化する際に多くのことが可能です。不必要な描画を減らし、ちらつきを避け、可能な限り最高のユーザー エクスペリエンスを追求します。もちろん、アプリケーション開発用に管理可能なフレームワークを作成することも考えられます。今回は、このような最適化の問題に取り組みます。

デスクトップ ウィンドウ

先月の ATL の例では、ATL CWindowImpl クラス テンプレートから派生するウィンドウ クラスを示しました。すべてがアプリケーションのウィンドウ クラス内にきちんと収まりました。しかし、最終的には、多くのウィンドウやレンダリングのコードが、そのウィンドウのアプリケーション固有のレンダリングとイベント処理に散在することになりました。この問題を解決するために、実用的な定型コードをできるだけたくさん基本クラスに押し込み、基本クラスがその定型コードを必要とするときに、コンパイル時のポリモーフィズムを使用して、アプリケーションのウィンドウ クラスまで到達するようにします。このアプローチは ATL や WTL で数多く使用されています。そのため、独自のクラスにもこの考え方を広げてみます。

図 2 にこの分離を示します。基本クラスは、DesktopWindow クラス テンプレートです。テンプレート パラメーターにより、基本クラスは仮想関数を使わないで具象クラスを呼び出せるようになります。今回の場合は、アプリケーションのウィンドウを呼び出し、実際の描画操作実行中の一連のレンダリング固有の前処理や後処理を隠ぺいするために、この技法を使用しています。DesktopWindow クラス テンプレートについてはこの後詳しく説明しますが、まず、そのウィンドウ クラスの登録に少し作業が必要です。

図 2 デスクトップ ウィンドウ

template <typename T>
class DesktopWindow :
  public CWindowImpl<DesktopWindow<T>, CWindow,
    CWinTraits<WS_OVERLAPPEDWINDOW | WS_VISIBLE>>
{
  BEGIN_MSG_MAP(DesktopWindow)
    MESSAGE_HANDLER(WM_PAINT, PaintHandler)
    MESSAGE_HANDLER(WM_DESTROY, DestroyHandler)
  END_MSG_MAP()
  LRESULT DestroyHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PostQuitMessage(0);
    return 0;
  }
  LRESULT PaintHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PAINTSTRUCT ps;
    VERIFY(BeginPaint(&ps));
    Render();
    EndPaint(&ps);
    return 0;
  }
  void Render()
  {
    ...
    static_cast<T *>(this)->Draw();
    ...
  }
    ...
};
struct SampleWindow : DesktopWindow<SampleWindow>
{
  void Draw()
  {
    ...
  }
};

ウィンドウ クラスの最適化

デスクトップ アプリケーション用 Windows API の現実の 1 つは、従来の USER リソースや GDI リソースを使って、レンダリングを簡略化するように設計されたことです。Direct2D に制御を任せ、目障りなちらつきにつながる不必要な描画を避けるためには、これらの "便利な" 機能の一部を無効にする必要があります。また、このような既定の機能の中には、Direct2D レンダリングに適した方法で動作するように、微調整する必要がある機能もあります。このような微調整の多くはウィンドウ クラスの情報を登録前に変更することで実現できますが、微調整する機能は ATL によってプログラマーにはわからないようになっています。さいわい、このような微調整を実現する方法は他にもあります。

先月、Windows API は、指定に基づいてウィンドウを作成する前にウィンドウ クラスの構造が登録されていることを前提とするしくみを紹介しました。ウィンドウ クラスの属性の 1 つに、背景ブラシがあります。Windows は、ウィンドウの描画を開始する前にこの GDI の背景ブラシを使用してウィンドウのクライアント領域を消去します。背景ブラシは、USER および GDI の時代は便利でしたが、Direct2D アプリケーションでは必要なくなり、一部のちらつきの原因にもなります。これを簡単に回避するには、ウィンドウ クラス構造の背景ブラシのハンドルに nullptr を設定します。比較的速い Windows 7 コンピューターや Windows 8 コンピューターで開発する場合は、ちらつきに気付かないため、この設定は必要ないと考えるかもしれません。それはただ、最近の Windows デスクトップがグラフィックス処理装置 (GPU) を基盤に効率的に構成されているため、ちらつきに気付きにくくなっているだけです。ただし、レンダリング パイプラインに大きな負荷をかけて、低速コンピューターで生じる可能性のある影響を引き起こすのは簡単です。ウィンドウの背景ブラシが白で、ウィンドウのクライアント領域をコントラストがはっきりとした黒のブラシで塗りつぶすとすると、10 ~ 100 ミリ秒ほどの短いスリープ遅延を加えることによって、ちらつきが発生するのがわかります。では、どのように防げばよいのでしょう。

前述のとおり、ウィンドウ クラスの登録で背景ブラシを指定しないと、Windows にはウィンドウをクリアするブラシがありません。しかし、お気づきかもしれませんが、ATL の例ではウィンドウ クラスの登録が完全に隠ぺいされています。一般的な解決策は、既定のウィンドウ処理 (DefWindowProc 関数) が対処する WM_ERASEBKGND メッセージを処理し、ウィンドウ クラスの背景ブラシでウィンドウのクライアント領域を塗りつぶす方法です。このメッセージを処理する場合、true を返すと、塗りつぶしは行われません。このメッセージは、ウィンドウ クラスに有効な背景ブラシが指定されているかどうかにかかわらずウィンドウに送信されるため、妥当な解決策と言えます。もう 1 つの解決策は、このような何の処理も行わないハンドラーを使うのを避け、最初の段階で、ウィンドウ クラスから背景ブラシを取り除く方法です。さいわい、ATL では、ウィンドウ作成のこの部分のオーバーライドが比較的簡単になります。ウィンドウの作成中、ATL により、ウィンドウの GetWndClassInfo メソッドが呼び出され、ウィンドウ クラスの情報が取得されます。このメソッドを独自に実装することもできますが、ATL には開発者に代わって実装を行う便利なマクロが用意されています。

DECLARE_WND_CLASS_EX(nullptr, CS_HREDRAW | CS_VREDRAW, -1);

このマクロの最後の引数はブラシ定数を意味しますが、値に -1 を指定すると、ウィンドウ クラス構造のブラシ属性がクリアされます。ウィンドウの背景が消去されたかどうかを判断する確実な方法は、WM_PAINT ハンドラー内部で BeginPaint 関数によって PAINTSTRUCT 構造体が設定されているのをチェックすることです。この構造体の fErase メンバーが false の場合、Windows によってウィンドウの背景が消去されているか、少なくとも、なんらかのコードが WM_ERASEBKGND メッセージに応答し、背景を消去したと主張していることがわかります。WM_ERASEBKGND メッセージ ハンドラーが背景を消去しない、またはできない場合、背景の消去は WM_PAINT メッセージ ハンドラーに委ねられます。ただし、ここで Direct2D を使用すれば、ウィンドウのクライアント領域のレンダリングを完全に制御して、このような二重の塗りつぶしを避けることができます。EndPain 関数を呼び出して、ウィンドウを実際に塗りつぶしたことを Windows に通知するのを忘れないでください。さもないと、Windows から WM_PAINT メッセージの不要なストリームが送り続けられることになります。これは、もちろん、アプリケーションのパフォーマンスを損ない、全体的な電力消費を増やすことになります。

ウィンドウ クラスの情報の注目すべき他の側面は、ウィンドウ クラスのスタイルです。これは、実は、先ほどのマクロの 2 番目の引数に当たります。CS_HREDRAW スタイルと CS_VREDRAW スタイルにより、ウィンドウが垂直方向と水平方向の両方にサイズ変更されるたびに、ウィンドウの無効化が引き起こされます。これはまったく必要ありません。たとえば、WM_SIZE メッセージを処理し、そこでウィンドウを無効化することもできますが、Windows がサイズ変更に対処し数行のコードを追加しなくて済むのなら大歓迎です。どちらにしても、ウィンドウを無効化しないと、その後、ウィンドウのサイズを小さくするときに、Windows からウィンドウに WM_PAINT メッセージが送信されなくなります。ウィンドウのコンテンツがクリップされても問題なければかまいませんが、最近は、ウィンドウのサイズに合わせて、さまざまなウィンドウ アセットを描画するのが一般的です。どちらを選ぶにしても、これは、アプリケーション ウィンドウにとって下さなければならない明確な決定事項です。

ウィンドウの背景についてのトピックに取り組むときは、多くの場合、ウィンドウを明示的に無効化することが望ましいと考えます。ウィンドウを明示的に無効化すれば、アプリケーション全体のさまざまな場所やさまざまなコード パスで描画処理を行う必要がなくなり、WM_PAINT メッセージに基づいたウィンドウのレンダリングを維持できます。マウスのクリックに応じて描画することがあります。もちろん、メッセージ ハンドラーでレンダリングを行うのが妥当です。ウィンドウを単純に無効化し、WM_PAINT ハンドラーでアプリケーションの現在状態をレンダリングしてもかまいません。これは、InvalidateRect 関数で行います。ATL では、InvalidateRect 関数をラップしただけの Invalidate メソッドを用意しています。InvalidateRect 関数について開発者が混乱しがちなのが、"erase" パラメーターの処理方法です。普通に考えると、消去の実行に「はい」と答えるとウィンドウが直ちに再描画され、「いいえ」と答えると何か異なる動きをすることになります。これは正しくありませんが、ドキュメントにもこのように記載されています。ウィンドウを無効化すると、直ちに再描画が行われます。erase オプションは、通常時、ウィンドウの背景を消去する、DefWindowProc 関数の代わりです。erase が true の場合、その後の BeginPaint 関数の呼び出しにより、ウィンドウの背景が消去されます。これも、WM_ERASEBKGND メッセージ ハンドラーを利用するのではなく、全体的にウィンドウ クラスの背景ブラシを避ける理由の 1 つです。背景ブラシがなければ、BeginPaint 関数には再描画に使用するブラシがないため、erase オプションは効果がありません。ATL を使うことでウィンドウ クラスの背景ブラシを設定することになる場合は、ウィンドウを無効化するときに注意が必要です。無効化によって再びちらつきが生まれるようになります。このため、今回の例では下記のプロテクト メンバーを、DesktopWindow クラス テンプレートに追加しました。

void Invalidate()
{
  VERIFY(InvalidateRect(nullptr, false));
}

ウィンドウを無効化するために、WM_DISPLAYCHANGE メッセージを処理するのも適切な考え方です。これにより、表示の変化によってウィンドウの外観がなんらかの影響を受ける場合、ウィンドウが適切に再描画されることが保証されます。

アプリの実行

アプリケーションの WinMain 関数は比較的シンプルに保つことをお勧めします。そのためには、パブリックの Run メソッドを DesktopWindow クラス テンプレートに追加し、ウィンドウ全体と Direct2D のファクトリ作成およびメッセージ ループを隠ぺいしました。DesktopWindow の Run メソッドを図 3 に示します。この Run メソッドにより、アプリケーションの WinMain 関数は非常にシンプルになります。

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  SampleWindow window;
  return window.Run();
}

図 3 DesktopWindow クラスの Run メソッド

int Run()
{
  D2D1_FACTORY_OPTIONS fo = {};
  #ifdef DEBUG
  fo.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
  #endif
  HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                       fo,
                       m_factory.GetAddressOf()));
  static_cast<T *>(this)->CreateDeviceIndependentResources();
  VERIFY(__super::Create(nullptr, nullptr, L"Direct2D"));
  MSG message;
  BOOL result;
  while (result = GetMessage(&message, 0, 0, 0))
  {
    if (-1 != result)
    {
      DispatchMessage(&message);
    }
  }
  return static_cast<int>(message.wParam);
}

ウィンドウを作成する前に、デバッグ ビルド用にデバッグ レイヤーを有効化することにより、Direct2D ファクトリ オプションを準備します。アプリケーションを開発するときは、Direct2D が有効なあらゆる診断をトーレスを実行できるように、同様のことを行うことを強くお勧めします。D2D1CreateFactory 関数はファクトリ インターフェイス ポインターを返します。このポインターを、DesktopWindow クラスのプロテクト メンバーである、Windows ランタイム C++ テンプレート ライブラリの優れた ComPtr スマート ポインターに渡します。次に CreateDeviceIndependentResources メソッドを呼び出し、デバイスに依存しない任意のリソース (ジオメトリやストロークのスタイルのようなアプリケーションの有効期間全体を通して再利用できるリソース) を作成します。このメソッドをオーバーライドする派生クラスも使用できますが、必要なければ、DesktopWindow クラス テンプレート内に空のスタブを用意します。最後に、簡単なメッセージ ループでブロックを作り、Run メソッドを完結します。メッセージ ループの説明については、先月号のコラムを確認してください。

レンダー ターゲット

Direct2D レンダー ターゲットは、WM_PAINT メッセージ ハンドラーの一環として呼び出される Render メソッドの内部に必要に応じて作成します。ここで作成するレンダー ターゲットでは、Direct2D の他の一部のレンダー ターゲットと異なり、デスクトップ ウィンドウにハードウェア アクセラレータによるレンダリングを提供するデバイス (ほとんどの場合は GPU) が消去されたり、レンダー ターゲットによって割り当てられるすべてのリソースを無効にするような方法で変更されたりする可能性があります。Direct2D には直接モードでレンダリングを行う性質があるため、どのリソースがデバイス固有で、随時作成し直さなければならないかを管理するのはアプリケーションの役割です。さいわい、これは簡単に管理できます。図 4 に、DesktopWindow の Render メソッド全体を示します。

図 4 DesktopWindow クラスの Render メソッド

void Render()
{
  if (!m_target)
  {
    RECT rect;
    VERIFY(GetClientRect(&rect));
    auto size = SizeU(rect.right, rect.bottom);
    HR(m_factory->CreateHwndRenderTarget(RenderTargetProperties(),
      HwndRenderTargetProperties(m_hWnd, size),
      m_target.GetAddressOf()));
    static_cast<T *>(this)->CreateDeviceResources();
  }
  if (!(D2D1_WINDOW_STATE_OCCLUDED & m_target->CheckWindowState()))
  {
    m_target->BeginDraw();
    static_cast<T *>(this)->Draw();
    if (D2DERR_RECREATE_TARGET == m_target->EndDraw())
    {
      m_target.Reset();
    }
  }
}

Render メソッドでは、まず、レンダー ターゲットの COM インターフェイスを管理している ComPtr が有効かどうかを確認します。この方法で、必要なときにのみレンダー ターゲットを作成し直します。ターゲットの作り直しは、ウィンドウが初めてレンダリングされるときに少なくとも 1 回は行われます。基盤となるデバイスに何かが行われる場合、またはどのような理由であれレンダー ターゲットを作成し直す必要がある場合、図 4 の Render メソッドの最後で、EndDraw メソッドから D2DERR_RECREATE_TARGET 定数が返されます。その後、ComPtr Reset メソッドを使用して、単純にレンダー ターゲットを解放します。次回、ウィンドウに自身を描画するように求められるときは、Render メソッドによって新しい Direct2D レンダー ターゲットが作成されることになります。

最初に、ウィンドウのクライアント領域を物理ピクセル単位で取得します。ほとんどの場合、Direct2D では高い DPI のディスプレイを自然にサポートできるよう、論理ピクセルだけが使用されます。ここから、物理ディスプレイと論理座標系の関係が始まります。次に、レンダー ターゲット オブジェクトを作成するための Direct2D ファクトリを呼び出します。この時点で、デバイス固有の任意のリソース (ブラシやビットマップなど、レンダー ターゲットの基盤となるデバイスに依存するリソース) を作成するため、派生したアプリケーション ウィンドウ クラスを呼び出します。繰り返しになりますが、必要なければ、空のスタブが DesktopWindow クラスによって提供されます。

描画前に、Render メソッドにより、ウィンドウが実際に表示され、まったく障害がないことをチェックします。これにより、あらゆる不必要なレンダリングが回避されます。通常、このチェックは、ユーザーによるデスクトップのロックや切り替えなど、基盤となる DirectX スワップ チェーンが非表示になるときのみ行われます。BeginDraw メソッドと EndDraw メソッドは、アプリケーション ウィンドウの Draw メソッド呼び出しを囲むように配置します。Direct2D は、GPU で最も優れたスループットとパフォーマンスを実現するため、頂点バッファー、結合描画コマンドなどでジオメトリをバッチ処理するチャンスを利用します。

Direct2D をデスクトップ ウィンドウと適切に統合するための最後の重要な手順は、ウィンドウのサイズが変更されるときに、レンダー ターゲットのサイズを変更することです。ウィンドウの適切な再描画が確実に行われるように、自動的に無効化が行われる方法は既に説明しました。しかし、レンダー ターゲット自身は、ウィンドウの寸法が変更されたことは認識しません。さいわい、図 5 で示すように、これは簡単に行えます。

図 5 レンダー ターゲットのサイズ変更

MESSAGE_HANDLER(WM_SIZE, SizeHandler)
LRESULT SizeHandler(UINT, WPARAM, LPARAM lparam, BOOL &)
{
  if (m_target)
  {
    if (S_OK != m_target->Resize(SizeU(LOWORD(lparam),
      HIWORD(lparam))))
    {
      m_target.Reset();
    }
  }
  return 0;
}

ComPtr が、現在有効なレンダー ターゲット COM インターフェイス ポインターを保持しているとすると、ウィンドウ メッセージの LPARAM によって提供される新たなサイズを指定して、レンダー ターゲットの Resize メソッドを呼び出します。なんらの理由で、レンダー ターゲットがサイズを変更できない内部リソースがある場合、ComPtr を単純にリセットして、次回レンダリングが要求されるときに、レンダー ターゲットを強制的に作り直します。

今回のコラムはここまでです。デスクトップ ウィンドウの作成と管理の両方を行うために必要なこと、GPU を使ってアプリケーションのウィンドウをレンダリングするために必要なことをすべて紹介しました。来月またお会いしましょう。引き続き、Direct2D を調査します。

Kenny Kerr はカナダに拠点を置くコンピューター プログラマーで、Pluralsight の執筆者、および Microsoft MVPでもあります。彼のブログは kennykerr.ca (英語) で、Twitter は twitter.com/kennykerr (英語) でフォローすることができます。

この記事のレビューに協力してくれた技術スタッフの Worachai Chaoweeraprasit に心より感謝いたします。