Windows と C++

Windows ランタイム用のレンダリング

Kenny Kerr

前回のコラムでは、Windows ランタイム (WinRT) アプリ モデルについて説明しました (msdn.microsoft.com/magazine/dn342867)。そこでは WinRT API 関数を少しだけ使用して、標準の C++ と従来の COM で Windows ストア アプリや Windows Phone アプリを作成する方法を紹介しました。C++/CX や C# などの言語プロジェクションを使用しなければならない要件はまったくありません。このような抽象化を回避できることは強力な機能で、このテクノロジのしくみを理解する優れた方法です。

2013 年 5 月のコラムでは、Direct2D 1.1 を導入し、これを使用してデスクトップ アプリケーションでレンダリングする方法を紹介しました (msdn.microsoft.com/magazine/dn198239、英語)。その次のコラムでは、C++ における DirectX プログラミングを大幅に簡略化する dx.h ライブラリ (dx.codeplex.com(英語) から利用可能) を取り上げました (msdn.microsoft.com/magazine/dn201741)。

前回のコラムのコードは、CoreWindow ベースのアプリを使用できるようにするには十分ですが、レンダリング機能はありません。

今月は、前回の基本スケルトンにレンダリングのサポートを追加する方法を説明します。WinRT アプリ モデルは、DirectX を使ったレンダリング向けに最適化されています。Direct2D および Direct3D のレンダリングに関する以前のコラムで説明したことを使って、CoreWindow ベースの WinRT アプリに応用する方法を、特に dx.h ライブラリからの Direct2D 1.1 の使用を中心に紹介します。記述する必要がある実際の Direct2D と Direct3D の描画コマンドは、デスクトップと Windows ランタイムのどちらを対象にするかに関わらず、大部分は同じです。ただし、いくつか小さな違いはあり、最初からすべてフックするのは非常に困難です。そこで、前回終了したところから始め、画面にピクセルを表示する方法を説明します。

レンダリングを適切にサポートするため、ウィンドウは特定のイベントを認識する必要があります。少なくとも、ウィンドウの可視性とサイズの変化や、ユーザーが選択した論理表示 DPI 構成の変化などを認識します。前回取り上げた Activated イベントと同様、これらの新しいイベントもすべて COM インターフェイス コールバックを通じてアプリに報告されます。ICoreWindow インターフェイスは、VisibilityChanged イベントおよび SizeChanged イベント用に登録するメソッドを提供しますが、まずそれぞれのハンドラーを実装する必要があります。実装する必要のある 2 つの COM インターフェイスは、Microsoft インターフェイス定義言語 (MIDL) が生成するクラス テンプレートの Activated イベント ハンドラーと非常によく似ています。

typedef ITypedEventHandler
   IVisibilityChangedEventHandler;
 typedef ITypedEventHandler
   IWindowSizeChangedEventHandler;

次に実装する必要がある COM インターフェイスは IDisplayPropertiesEventHandler で、さいわい既に定義されています。次のように、関連するヘッダー ファイルをインクルードするだけです。

#include

また、関連する型は次の名前空間で定義されます。

using namespace ABI::Windows::Graphics::Display;

これらの定義を使用して、前回のコラムの SampleWindow クラスを更新し、これら 3 つのインターフェイスも継承します。

struct SampleWindow :
   ...
   IVisibilityChangedEventHandler,
   IWindowSizeChangedEventHandler,
   IDisplayPropertiesEventHandler

QueryInterface 実装を更新し、これらのインターフェイスのサポートを示すことも必要です。これは課題としてやってみてください。当然、前回指摘したように、これらの COM インターフェイス コールバックをどこで実装しても Windows ランタイムにとっては同じです。つまり、Windows ランタイムは、アプリの IFrameworkView (SampleWindow クラスが実装する主要インターフェイス) がこれらのコールバック インターフェイスも同様に実装するとは想定しません。そのため、QueryInterface プロパティは確かにこれらのインターフェイスのクエリを処理しますが、Windows ランタイムはこれらをクエリしません。代わりに、それぞれのイベントを登録する必要があり、これを行うには IFrameworkView Load メソッドの実装が最適です。この Load メソッドにすべてのコードを追加して、最初の画面を表示するアプリを準備します。次に、Load メソッド内で VisibilityChanged イベントと SizeChanged イベントを登録します。

EventRegistrationToken token;
 HR(m_window->add_VisibilityChanged(this, &token));
 HR(m_window->add_SizeChanged(this, &token));

このコードは、Windows ランタイムに最初の 2 つのインターフェイスの実装を見つける場所を明示的に伝えます。第 3 および最後のインターフェイスは LogicalDpiChanged イベント用ですが、このイベント登録は IDisplayPropertiesStatics インターフェイスで提供されます。この静的インターフェイスは WinRT DisplayProperties クラスによって実装されますが、このインターフェイスは、単に GetActivationFactory 関数テンプレートを使って取得します (GetActivationFactory の実装は前回のコラムで紹介しました)。

ComPtr m_displayProperties;
 m_displayProperties = GetActivationFactory(
   RuntimeClass_Windows_Graphics_Display_DisplayProperties);

このインターフェイス ポインターは、ウィンドウのライフサイクル中のさまざまな時点で呼び出すことになるため、メンバー変数に保持します。今のところ、Load メソッド内で LogicalDpiChanged イベントを登録するだけです。

HR(m_displayProperties->add_LogicalDpiChanged(this, &token));

これら 3 つのインターフェイスの実装には、すぐ後で戻ります。次に、DirectX インフラストラクチャを準備します。これまでのコラムで繰り返し説明してきたデバイス リソース ハンドラーの標準セットが必要になります。

void CreateDeviceIndependentResources() {}
 void CreateDeviceSizeResources() {}
 void CreateDeviceResources() {}
 void ReleaseDeviceResources() {}

1 つ目は、基盤となる Direct3D レンダリング デバイス固有ではないあらゆるリソースを作成または呼び出します。次の 2 つは、デバイス固有のリソースの作成用です。ウィンドウ サイズ固有のリソースは、それ以外のリソースから区別するようにします。最後に、すべてのデバイス リソースを解放する必要があります。残りの DirectX インフラストラクチャは、正確に言えば、アプリ固有のニーズに基づいてこれら 4 つのメソッドを実装するアプリによって異なります。このインフラストラクチャは、リソースのレンダリングと、これらのリソースの効率的な作成と再利用を管理するためのポイントをアプリごとに提供します。

これで、dx.h を使用して DirectX の厄介な作業をすべて処理できるようになります。

#include "dx.h"

すべての Direct2D アプリは Direct2D ファクトリから開始します。

Factory1 m_factory;

これは Direct2D 名前空間にあり、通常は次のようにインルードします。

using namespace KennyKerr;
 using namespace KennyKerr::Direct2D;

dx.h ライブラリには、Direct2D、DirectWrite、Direct3D、Microsoft DirectX Graphics Infrastructure (DXGI) などの名前空間が個別に用意されています。今回のアプリのほとんどは Direct2D を多く使用するため、今回の場合はこのしくみが適していて、アプリにとって意味のある方法で名前空間を管理できます。

m_factory メンバー変数は、Direct2D 1.1 ファクトリを表します。必要に応じて、レンダー ターゲットなどのデバイスに依存しないさまざまなリソースの作成にも使用します。Load メソッドの最後の手順として、Direct2D ファクトリを作成し、デバイスに依存しないリソースを作成できるようにします。

m_factory = CreateFactory();
 CreateDeviceIndependentResources();

Load メソッドから戻ったら、WinRT CoreApplication クラスはすぐに IFrameworkView Run メソッドを呼び出します。

前回のコラムの SampleWindow Run メソッドの実装をブロックするには、CoreWindow ディスパッチャーの ProcessEvents メソッドを呼び出すだけです。さまざまなイベントが発生したときだけレンダリングするのであれば、この方法でブロックすれば十分です。たとえば、アプリにゲームを実装していたり、高解像度のアニメーションが必要なだけといった場合です。これとは反対の極端な例は、連続アニメーション ループを使用する場合ですが、このような場合はおそらくもう少しインテリジェントなものが必要になります。今回は、2 つの例の妥協点のようなものを実装します。まず、ウィンドウが可視かどうかを追跡するメンバー変数を追加します。これにより、ウィンドウがユーザーに物理的に表示されていないときは、レンダリングを抑制します。

bool m_visible;
 SampleWindow() : m_visible(true) {}

次に、Run メソッドを書き直します (図 1 参照)。

図 1 動的なレンダリング ループ

auto __stdcall Run() -> HRESULT override
 {
   ComPtr dispatcher;
   HR(m_window->get_Dispatcher(dispatcher.GetAddressOf()));
   while (true)
   {
     if (m_visible)
     {
       Render();
       HR(dispatcher->
         ProcessEvents(CoreProcessEventsOption_ProcessAllIfPresent));
     }
     else
     {
       HR(dispatcher->
         ProcessEvents(CoreProcessEventsOption_ProcessOneAndAllPending));
     }
   }
   return S_OK;
 }

前と同様、Run メソッドは CoreWindow ディスパッチャーを取得します。次に無限ループに入り、キュー内のあらゆるウィンドウ メッセージ (Windows ランタイムでは "イベント" と呼びます) のレンダリングと処理を継続して行います。ただし、ウィンドウが表示されていなければ、メッセージが到着するまでブロックします。ウィンドウの可視性が変化するタイミングをどのように把握すればよいでしょう。このためにあるのが IVisibilityChangedEventHandler インターフェイスです。ここで、このインターフェイスの Invoke メソッドを実装して m_visible メンバー変数を更新します。

auto __stdcall Invoke(ICoreWindow *,
   IVisibilityChangedEventArgs * args) -> HRESULT override
 {
   unsigned char visible;
   HR(args->get_Visible(&visible));
   m_visible = 0 != visible;
   return S_OK;
 }

MIDL が生成したインターフェイスは、移植可能な Boolean データ型として符号なし char 型を使用します。既に用意した IVisibilityChangedEventArgs インターフェイス ポインターを使用して単にウィンドウの現在の可視状態を取得し、それに応じてメンバー変数を更新します。このイベントはウィンドウの可視性が変化したときに必ず発生し、デスクトップ アプリケーション用の実装よりも少しシンプルです。これは、ウィンドウの切り替えはもちろん、アプリのシャットダウンや電源管理などのさまざまなシナリオを処理するためです。

次に、図 1 の Run メソッドから呼び出される Render メソッドを実装する必要があります。このメソッドは、オンデマンドでレンダリング スタックを作成し、実際の描画コマンドを実行します。基本スケルトンを図 2 に示します。

図 2 Render メソッドの概要

void Render()
 {
   if (!m_target)
   {
     // Prepare render target ...
   }
   m_target.BeginDraw();
   Draw();
   m_target.EndDraw();
   auto const hr = m_swapChain.Present();
   if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr)
   {
     ReleaseDevice();
   }
 }

Render メソッドはなじみがあるでしょう。Direct2D 1.1 向けの概要を既に示しましたが、その場合と同じ基本形式です。必要に応じて、まずレンダー ターゲットを作成します。その直後の BeginDraw と EndDraw の呼び出しの間で実際の描画コマンドを実行します。レンダー ターゲットは Direct2D デバイス コンテキストなので、実際にレンダリングされたピクセルを画面に表示するには、スワップ チェーンを提供する必要があります。また、Direct2D 1.1 デバイス コンテキストと DirectX 11.1 バージョンのスワップ チェーンを表す dx.h 型の追加も必要です。後者は、Dxgi 名前空間にあります。

DeviceContext m_target;
 Dxgi::SwapChain1 m_swapChain;

Render メソッドは、表示に失敗した場合に ReleaseDevice を呼び出して完了します。

void ReleaseDevice()
 {
   m_target.Reset();
   m_swapChain.Reset();
   ReleaseDeviceResources();
 }

このメソッドは、レンダー ターゲットとスワップ チェーンを解放します。ReleaseDeviceResources も呼び出して、ブラシ、ビットマップ、効果などのデバイス固有のリソースも解放します。この ReleaseDevice メソッドは重要ではないように思われるかもしれませんが、DirectX アプリでデバイスが利用できない状況を確実に処理するために重要です。すべてのデバイス リソース (GPU が提供するすべてのリソース) を正しく解放しないと、デバイスを利用できなくなった状況から回復できず、アプリはクラッシュします。

次に、図 2 の Render メソッドでは省略したレンダー ターゲットを準備します。まず、Direct3D デバイスを作成します (dx.h ライブラリは次のいくつかの手順も大きく簡略化します)。

auto device = Direct3D::CreateDevice();

Direct3D デバイスを利用している場合でも、Direct2D ファクトリに切り替えて、Direct2D デバイスと Direct2D デバイス コンテキストを作成できます。

m_target = m_factory.CreateDevice(device).CreateDeviceContext();

次に、ウィンドウのスワップ チェーンを作成します。まず、Direct3D デバイスから DXGI ファクトリを取得します。

auto dxgi = device.GetDxgiFactory();

次に、アプリの CoreWindow 用のスワップ チェーンを作成します。

m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());

ここでも、dx.h ライブラリは DXGI_SWAP_CHAIN_DESC1 構造体を自動的に作成することで、作業を大幅に簡略化します。次に、CreateDeviceSwapChainBitmap メソッドを呼び出し、スワップ チェーンのバック バッファを表す Direct2D ビットマップを作成します。

void CreateDeviceSwapChainBitmap()
 {
   BitmapProperties1 props(BitmapOptions::Target | BitmapOptions::CannotDraw,
     PixelFormat(Dxgi::Format::B8G8R8A8_UNORM, AlphaMode::Ignore));
   auto bitmap =
     m_target.CreateBitmapFromDxgiSurface(m_swapChain, props);
   m_target.SetTarget(bitmap);
 }

このメソッドでは、まずスワップ チェーンのバック バッファを Direct2D にとって意味を持つように表す必要があります。BitmapProperties1 は、Direct2D D2D1_BITMAP_PROPERTIES1 構造体の dx.h バージョンです。BitmapOptions::Target 定数は、ビットマップがデバイス コンテキストのターゲットとして使用されることを示します。BitmapOptions::CannotDraw 定数は、スワップ チェーンのバック バッファが出力としてのみ使用され、他の描画操作の入力としては使用されないという事実に関連しています。PixelFormat は、Direct2D D2D1_PIXEL_FORMAT 構造体の dx.h バージョンです。

定義したビットマップのプロパティを使用して、CreateBitmapFromDxgiSurface メソッドはスワップ チェーンのバック バッファを取得し、これを表示するため Direct2D ビットマップを作成します。このようにして、Direct2D デバイス コンテキストは、単に SetTarget メソッドでビットマップをターゲットにすることで、スワップ チェーンに直接レンダリングします。

Render メソッドに戻り、Direct2D にユーザーの DPI 構成に従って描画コマンドを拡張する方法を指示します。

float dpi;
 HR(m_displayProperties->get_LogicalDpi(&dpi));
 m_target.SetDpi(dpi);

次にアプリのデバイス リソース ハンドラーを呼び出し、必要に応じてリソースを作成します。図 3 は Render メソッド用の完成したデバイス初期化シーケンスをまとめたものです。

図 3 レンダー ターゲットの準備

void Render()
 {
   if (!m_target)
   {
     auto device = Direct3D::CreateDevice();
     m_target = m_factory.CreateDevice(device).CreateDeviceContext();
     auto dxgi = device.GetDxgiFactory();
     m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());
     CreateDeviceSwapChainBitmap();
     float dpi;
     HR(m_displayProperties->get_LogicalDpi(&dpi));
     m_target.SetDpi(dpi);
     CreateDeviceResources();
     CreateDeviceSizeResources();
   }
   // Drawing and presentation ... see Figure 2

DPI スケーリングは Direct2D デバイス コンテキスト作成直後に適用するプロパティですが、この設定がユーザーによって変更されたときも必ず更新する必要があります。DPI スケーリングを実行中のアプリ用に変更できるのは、Windows 8 の新機能です。ここで、IDisplayPropertiesEventHandler インターフェイスが登場します。これで、単に Invoke メソッドを実装し、必要に応じてデバイスを更新できます。以下に、LogicalDpiChanged イベント ハンドラーを示します。

auto __stdcall Invoke(IInspectable *) -> HRESULT override
 {
   if (m_target)
   {
     float dpi;
     HR(m_displayProperties->get_LogicalDpi(&dpi));
     m_target.SetDpi(dpi);
     CreateDeviceSizeResources();
     Render();
   }
   return S_OK;
 }

ターゲット (デバイス コンテキスト) が作成されている場合、現在の論理 DPI 値を取得し、単に Direct2D に転送します。次に、アプリを呼び出してレンダリングし直す前に任意のデバイス サイズ固有のリソースを再作成します。このようにして、アプリはディスプレイの DPI 構成の変化に動的に対応できます。ウィンドウが動的に処理する必要のある最後の変化は、ウィンドウ サイズの変更です。既にイベント登録をフックしているため、SizeChanged イベント ハンドラーを表す IWindowSizeChangedEventHandler Invoke メソッドの実装を追加するだけです。

auto __stdcall Invoke(ICoreWindow *,
   IWindowSizeChangedEventArgs *) -> HRESULT override
 {
   if (m_target)
   {
     ResizeSwapChainBitmap();
     Render();
   }
   return S_OK;
 }

最後に、ResizeSwapChainBitmap メソッドでスワップ チェーン ビットマップのサイズを変更します。これも、慎重に処理する必要があります。スワップ チェーンのバッファのサイズ変更は、効率よく操作しなければなりませんが、そのためには適切に実行する必要があります。まず、この操作を完了するため、これらのバッファへの参照をすべて解放します。これらの参照はアプリが直接または間接的に取得している可能性があります。今回の場合、参照は Direct2D デバイス コンテキストによって保持されています。ターゲット イメージは、スワップ チェーンのバック バッファをラップするために作成した Direct2D ビットマップです。この解放は非常に簡単です。

m_target.SetTarget();

次に、スワップ チェーンの ResizeBuffers メソッドを呼び出して、すべての厄介な作業を行い、必要に応じてアプリのデバイス リソース ハンドラーを呼び出します。図 4は、これを組み合わせる方法を示しています。

図 4 スワップ チェーンのサイズ変更

void ResizeSwapChainBitmap()
 {
   m_target.SetTarget();
   if (S_OK == m_swapChain.ResizeBuffers())
   {
     CreateDeviceSwapChainBitmap();
     CreateDeviceSizeResources();
   }
   else
   {
     ReleaseDevice();
   }
 }

これで、いくつか描画コマンドを追加できるようになり、DirectX によって CoreWindow のターゲットに効果的にレンダリングされます。単純な例として、次のように CreateDeviceResources ハンドラー内に単色ブラシを作成し、メンバー変数に割り当てるとします。

SolidColorBrush m_brush;
 m_brush = m_target.CreateSolidColorBrush(Color(1.0f, 0.0f, 0.0f));

ウィンドウの Draw メソッド内で、まずウィンドウの背景を白にします。

m_target.Clear(Color(1.0f, 1.0f, 1.0f));

次に、ブラシを使って単純な赤の四角形を描画します。

RectF rect (100.0f, 100.0f, 200.0f, 200.0f);
 m_target.DrawRectangle(rect, m_brush);

デバイスが利用できなくなったときに適切に回復できるように、適切なタイミングでブラシを解放する必要があります。

void ReleaseDeviceResources()
 {
   m_brush.Reset();
 }

DirectX で CoreWindow ベースのアプリをレンダリングするために必要なのはこれですべてです。当然、今回のコラムを 2013 年 5 月号のコラムと比較すると、dx.h ライブラリのおかげで DirectX 関連のコードが非常にシンプルになったことに、うれしい驚きを覚えるでしょう。しかし、主に COM インターフェイスの実装に関連するスケルトン コードがかなりあります。ここで C++/CX が登場し、アプリ内の WinRT API の使用を簡略化します。これは、過去 2 回のコラムで示したいくつかのスケルトン COM コードを隠します。

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