Direct2D によるレイヤード ウィンドウ
Kenny Kerr
Direct2D に関する 3 回目のコラムとなる今回は、相互運用性に比類なき能力を発揮する機能の一部を紹介します。Direct2D が提供するさまざまな相互運用性オプションをすべて詳しく説明するのではなく、実用的なアプリケーションであるレイヤード ウィンドウについて説明します。レイヤード ウィンドウは、古くからあるウィンドウ機能の 1 つですが、あまり進化していないため、最新のグラフィックス テクノロジと効果的に併用するには特別な注意が必要です。
このコラムは、Direct2D プログラミングについて基本的な知識があることが前提です。基本的な知識をお持ちでない方は、Direct2D を使用したプログラミングと描画の基本事項を紹介した、6 月号のコラム (msdn.microsoft.com/ja-jp/magazine/dd861344) と 9 月号のコラム (msdn.microsoft.com/magazine/ee413543、英語) をお読みいただくことをお勧めします。
本来、レイヤード ウィンドウは、いくつか異なる目的に使用されてきました。具体的には、レイヤード ウィンドウを使用すると、視覚効果やちらつきのないレンダリングを簡単かつ効率的に実現できます。GDI がグラフィックスを作成する際の主流だった頃は、これは本当に思いがけず嬉しいものでした。ところが、ハードウェア アクセラレータが登場した現在では、こうした魅力は色あせてしまいました。レイヤード ウィンドウでは、まだ User32/GDI が扱われており、パフォーマンスや品質の高いグラフィックスを実現するマイクロソフト プラットフォームである DirectX をサポートするという、重要な更新が行われていないためです。
レイヤード ウィンドウには、ピクセル単位にアルファ ブレンドを行ってデスクトップ上のウィンドウを組み立てるという独特の機能があります。こうした機能は、Windows SDK を使う方法では実現できません。
実際のレイヤード ウィンドウには、2 種類の方式があります。その違いは、ピクセル単位に不透明度を制御するか、ウィンドウ全体で不透明度を制御するかにあります。今回のコラムでは、前者について説明しますが、実際にウィンドウ単位に不透明度を制御する場合でも、ウィンドウを作成してから SetLayeredWindowAttributes 関数を呼び出してアルファ値を設定するだけです。
Verify(SetLayeredWindowAttributes(
windowHandle,
0, // no color key
180, // alpha value
LWA_ALPHA));
ここでは、WS_EX_LAYERED 拡張スタイルを指定してウィンドウを作成しているか、SetWindowLong 関数の使用後にこのスタイルを適用していることを前提とします。このようなウィンドウの例を図 1 に示します。このメリットは明白で、デスクトップ ウィンドウ マネージャー (DWM) によって自動的にウィンドウが適切にブレンドされるため、アプリケーションでウィンドウを描画する方法を何も変更する必要はありません。そうでなければ、自分ですべてを完全に描画する必要があります。もちろん、Direct2D などの新しいレンダリング テクノロジを使用していれば問題ありません。
図 1 アルファ値によるウィンドウ
では、どのようなことが必要でしょうか。根本的なレベルでは簡単です。まず、UPDATELAYEREDWINDOWINFO 構造体に値を設定します。この構造体は、レイヤード ウィンドウの位置とサイズと、ウィンドウの表示面を定義する GDI デバイス コンテキスト (DC) を提供します。問題はここにあります。DC は、GDI という古い環境を使用しているため、DirectX やハードウェア アクセラレーションを使用する環境とは大きく異なります。これについてはすぐに説明します。
自身で割り当てる必要がある構造体へのポインターがいっぱいあることに加え、Windows SDK には、UPDATELAYEREDWINDOWINFO 構造体についての十分な説明がないため、使用方法はあまり明確になっていません。全体では、5 つの構造体を割り当てる必要があります。DC からコピーするビットマップの位置を特定するソース位置の構造体、更新時にウィンドウが配置されるデスクトップ上の位置を特定するウィンドウ位置の構造体、コピーするビットマップのサイズ (ウィンドウのサイズも定義します) を示す構造体があります。
POINT sourcePosition = {};
POINT windowPosition = {};
SIZE size = { 600, 400 };
また、レイヤード ウィンドウをデスクトップとブレンドする方法を定義する BLENDFUNCTION 構造体があります。これは、見過ごされがちですが、驚くほど万能な構造体で、非常に便利です。通常、次のように設定します。
BLENDFUNCTION blend = {};
blend.SourceConstantAlpha = 255;
blend.AlphaFormat = AC_SRC_ALPHA;
AC_SRC_ALPHA 定数は、ソース ビットマップにアルファ チャネルが含まれていることを示します。これが最も一般的なシナリオです。
ただし、興味深いことに、SourceConstantAlpha は、SetLayeredWindowAttributes 関数を使用してウィンドウ全体の不透明度を制御することとほぼ同じです。SourceConstantAlpha に 255 を設定すると、レイヤード ウィンドウはピクセル単位にアルファ値を使用します。しかし、これを完全にゼロ (完全に透明) にすると、再描画の手間をかけることなく、ウィンドウのフェード インまたはフェード アウトなどの効果を実現できます。これで、BLENDFUNCTION 構造体の名前の由来が明らかになったのではないでしょうか。アルファ ブレンドを適用した結果のウィンドウは、この構造体の値の関数になります。
最後の構造体は、すべてを結び付ける UPDATELAYEREDWINDOWINFO です。
UPDATELAYEREDWINDOWINFO info = {};
info.cbSize = sizeof(UPDATELAYEREDWINDOWINFO);
info.pptSrc = &sourcePosition;
info.pptDst = &windowPosition;
info.psize = &size;
info.pblend = &blend;
info.dwFlags = ULW_ALPHA;
これについては、改めて説明することもなく一目瞭然です。説明していないメンバーは dwFlags 変数だけです。このメンバーの値 ULW_ALPHA は、以前に古い UpdateLayeredWindow 関数を使用したことがあれば見覚えがあるはずです。これは、ブレンド関数を使用することを示しているだけです。
最後に、ソース DC へのハンドルを指定し、UpdateLayeredWindowIndirect 関数を呼び出してウィンドウを更新する必要があります。
info.hdcSrc = sourceDC;
Verify(UpdateLayeredWindowIndirect(
windowHandle, &info));
これだけです。ウィンドウは、WM_PAINT メッセージを受け取りません。ウィンドウの表示または更新が必要になるたびに、UpdateLayeredWindowIndirect 関数を呼び出すだけです。この定型コードを使用しないようにするには、図 2 に示す LayeredWindowInfo ラッパー クラスを使用します。
図 2 LayeredWindowInfo ラッパー クラス
class LayeredWindowInfo {
const POINT m_sourcePosition;
POINT m_windowPosition;
CSize m_size;
BLENDFUNCTION m_blend;
UPDATELAYEREDWINDOWINFO m_info;
public:
LayeredWindowInfo(
__in UINT width,
__in UINT height) :
m_sourcePosition(),
m_windowPosition(),
m_size(width, height),
m_blend(),
m_info() {
m_info.cbSize = sizeof(UPDATELAYEREDWINDOWINFO);
m_info.pptSrc = &m_sourcePosition;
m_info.pptDst = &m_windowPosition;
m_info.psize = &m_size;
m_info.pblend = &m_blend;
m_info.dwFlags = ULW_ALPHA;
m_blend.SourceConstantAlpha = 255;
m_blend.AlphaFormat = AC_SRC_ALPHA;
}
void Update(
__in HWND window,
__in HDC source) {
m_info.hdcSrc = source;
Verify(UpdateLayeredWindowIndirect(window, &m_info));
}
UINT GetWidth() const { return m_size.cx; }
UINT GetHeight() const { return m_size.cy; }
};
図 3 は、ATL/WTL と図 2 の LayeredWindowInfo ラッパー クラスを使用したレイヤード ウィンドウの基本構造を示しています。最初に注目するのは、WM_PAINT を使用しないため、UpdateWindow を呼び出す必要がないことです。代わりに、すぐに Render メソッドを呼び出します。このメソッドは、次になんらかの描画を実行したり、LayeredWindowInfo の Update メソッドに DC を指定したりするのに必要です。おもしろくなるのは、描画がどのように行われ、DC がどこから取得されるかというところからです。
図 3 レイヤード ウィンドウの構造
class LayeredWindow :
public CWindowImpl<LayeredWindow,
CWindow, CWinTraits<WS_POPUP, WS_EX_LAYERED>> {
LayeredWindowInfo m_info;
public:
BEGIN_MSG_MAP(LayeredWindow)
MSG_WM_DESTROY(OnDestroy)
END_MSG_MAP()
LayeredWindow() :
m_info(600, 400) {
Verify(0 != __super::Create(0)); // parent
ShowWindow(SW_SHOW);
Render();
}
void Render() {
// Do some drawing here
m_info.Update(m_hWnd,
/* source DC goes here */);
}
void OnDestroy() {
PostQuitMessage(1);
}
};
GDI/GDI+ の方法
最初に、GDI/GDI+ の描画のしくみを示します。まず、青、緑、赤、アルファ (BGRA) の順にバイト列を並べたカラー チャネルを使用して、プリマルチプライ処理済みの 32 ビット/ピクセル (bpp) のビットマップを作成します。プリマルチプライ処理済みとは、カラー チャネル値とアルファ値があらかじめ乗算されていることを意味します。これにより、アルファ ブレンドを行うイメージのパフオーマンスが向上しますが、本来のカラー値を取得する際に、カラー値をアルファ値で除算するという逆の処理を実行しなければならないことを意味します。GDI 用語では、これは 32 bpp のデバイスに依存しないビットマップ (DIB) と呼ばれ、BITMAPINFO 構造体に情報を指定し、その構造体を CreateDIBSection 関数に渡すことで作成されます (図 4 参照)。
図 4 DIB の作成
BITMAPINFO bitmapInfo = {};
bitmapInfo.bmiHeader.biSize =
sizeof(bitmapInfo.bmiHeader);
bitmapInfo.bmiHeader.biWidth =
m_info.GetWidth();
bitmapInfo.bmiHeader.biHeight =
0 – m_info.GetHeight();
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression =
BI_RGB;
void* bits = 0;
CBitmap bitmap(CreateDIBSection(
0, // no DC palette
&bitmapInfo,
DIB_RGB_COLORS,
&bits,
0, // no file mapping object
0)); // no file offset
ここには細かい情報がたくさんありますが、今回の説明には関係ありません。この API 関数は古くからあります。注意すべき点は、ビットマップの高さに負の値を指定していることです。BITMAPINFOHEADER 構造体では、ボトムアップまたはトップダウンのいずれかでビットマップを定義します。高さが正であればボトムアップのビットマップになり、負であればトップダウンのビットマップになります。トップダウン ビットマップの原点は左上隅で、ボトムダウン ビットマップの原点は左下隅です。
ここでトップダウンを指定することは厳密には必要ではありませんが、Windows での最近のイメージング コンポーネントのほとんどがトップダウンのビットマップを使用する傾向があるため、このように指定しておくと相互運用性が向上します。これにより、ストライドが正になり、次のように計算します。
UINT stride = (width * 32 + 31) / 32 * 4;
これで、ビット ポインターを介してビットマップの描画を開始するための十分な情報が揃いました。当然、支障がない限り、なんらかの描画関数を使用することになりますが、残念なことに、GDI が提供する関数の多くは、アルファ チャネルをサポートしません。そこで、GDI+ の登場となります。
ビットマップ データを直接 GDI+ に渡してもかまいませんが、UpdateLayeredWindowIndirect 関数に渡すためにはどうしても DC が必要になるため、DC を作成しましょう。DC を作成するには、CreateCompatibleDC というわかりやすい名前の関数を呼び出し、デスクトップと互換性のあるメモリ DC を作成します。その後、SelectObject 関数を呼び出して DC に書き込むビットマップを選択できます。図 5 に示す GdiBitmap ラッパー クラスは、これらの処理をすべてラップしたうえで、追加のハウスキーピング処理も提供します。
図 5 DIB ラッパー クラス
class GdiBitmap {
const UINT m_width;
const UINT m_height;
const UINT m_stride;
void* m_bits;
HBITMAP m_oldBitmap;
CDC m_dc;
CBitmap m_bitmap;
public:
GdiBitmap(__in UINT width,
__in UINT height) :
m_width(width),
m_height(height),
m_stride((width * 32 + 31) / 32 * 4),
m_bits(0),
m_oldBitmap(0) {
BITMAPINFO bitmapInfo = { };
bitmapInfo.bmiHeader.biSize =
sizeof(bitmapInfo.bmiHeader);
bitmapInfo.bmiHeader.biWidth =
width;
bitmapInfo.bmiHeader.biHeight =
0 - height;
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression =
BI_RGB;
m_bitmap.Attach(CreateDIBSection(
0, // device context
&bitmapInfo,
DIB_RGB_COLORS,
&m_bits,
0, // file mapping object
0)); // file offset
if (0 == m_bits) {
throw bad_alloc();
}
if (0 == m_dc.CreateCompatibleDC()) {
throw bad_alloc();
}
m_oldBitmap = m_dc.SelectBitmap(m_bitmap);
}
~GdiBitmap() {
m_dc.SelectBitmap(m_oldBitmap);
}
UINT GetWidth() const {
return m_width;
}
UINT GetHeight() const {
return m_height;
}
UINT GetStride() const {
return m_stride;
}
void* GetBits() const {
return m_bits;
}
HDC GetDC() const {
return m_dc;
}
};
デバイスに描画するためのメソッドを提供する GDI+ Graphics クラスは、ビットマップの DC を使用して構築できます。図 6 は、図 3 の LayeredWindow クラスを更新して、GDI+ によるレンダリングをサポートする方法を示します。定型の GDI コードすべてを除くと、このコードはかなり簡単です。ウィンドウのサイズを GdiBitmap コンストラクターに渡し、ビットマップの DC を Graphics コンストラクターと Update メソッドに渡します。簡単ですが、GDI も GDI+ も (ほとんど) ハードウェア アクセラレータが使用されておらず、特に強力なレンダリング機能も提供していません。
図 6 GDI レイヤード ウィンドウ
class LayeredWindow :
public CWindowImpl< ... {
LayeredWindowInfo m_info;
GdiBitmap m_bitmap;
Graphics m_graphics;
public:
LayeredWindow() :
m_info(600, 400),
m_bitmap(m_info.GetWidth(), m_info.GetHeight()),
m_graphics(m_bitmap.GetDC()) {
...
}
void Render() {
// Do some drawing with m_graphics object
m_info.Update(m_hWnd,
m_bitmap.GetDC());
}
...
アーキテクチャの問題
対照的に、Windows Presentation Foundation (WPF) では、次のようにレイヤード ウィンドウを作成するだけです。
class LayeredWindow : Window {
public LayeredWindow() {
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
// Do some drawing here
}
}
これは、驚くほど簡単ですが、レイヤード ウィンドウに関連する複雑性やレイヤード ウィンドウを使用する際のアーキテクチャの制限に反します。どんなに簡単に見えても、レイヤード ウィンドウは、このコラムで概要を説明したアーキテクチャの原則に従わなければなりません。WPF ではレンダリングにハードウェア アクセラレータを使用できることもありますが、UpdateLayeredWindowIndirect 関数を呼び出して表示を更新する前に、互換性のある DC に選択したプリマルチプライ処理済み BGRA ビットマップに結果をコピーする必要があります。WPF はブール型の変数以外は公開していないため、代わりに特定の選択を行う必要があり、この選択を制御することはできません。なぜ問題なのでしょうか。問題はハードウェアです。
グラフィック プロセッシング ユニット (GPU) には、専用メモリを使って最適なパフォーマンスを実現するメリットがあります。つまり、既存のビットマップを操作する必要がある場合は、そのビットマップをシステム メモリ (RAM) から GPU メモリにコピーする必要があります。このコピーは、多くの場合、システム メモリどうしでコピーを行うよりも時間がかかります。逆も同じです。GPU を使用してビットマップを作成およびレンダリングする場合、そのビットマップをシステム メモリにコピーすることになると、これも負荷の高いコピー操作です。
通常、GPU がレンダリングしたビットマップはディスプレイ デバイスに直接送信されるため、この問題は発生しません。レイヤード ウィンドウの場合、User32/GDI リソースはカーネル モード リソースにもユーザー モード リソースにも関係し、どちらもビットマップへのアクセスを必要とするため、ビットマップをシステム メモリに戻さなければなりません。たとえば、User32 がレイヤード ウィンドウのヒット テストを実行する必要があるとします。レイヤード ウィンドウのヒット テストは、ビットマップのアルファ値に基づいており、特定の点のピクセルが透明であっても、マウス メッセージを送信できます。そのため、これを可能にするには、システム メモリ内にビットマップのコピーが必要です。ビットマップが UpdateLayeredWindowIndirect によってコピーされたら、DWM がデスクトップを構成できるように、GPU に直接戻されます。
GPU とシステム間でメモリをコピーするコストに加え、GPU を CPU と同期するコストもかかります。CPU を集中的に使用する一般的な操作とは異なり、GPU の操作は非同期に行われる傾向があります。これにより、一連のレンダリング コマンドをバッチ処理する際に優れたパフォーマンスが実現します。CPU とのやり取りが発生すると、そのたびにバッチ処理コマンドがフラッシュされ、CPU は GPU が処理を完了するまで待機することになり、結果として、パフォーマンスは最適とは言えなくなります。
つまり、このようなラウンドトリップやその頻度とコストについて注意する必要があります。レンダリングするシーンが複雑すぎると、ビットマップのコピーにかかるコストが、ハードウェア アクセラレータによって得られるパフォーマンスのメリットを簡単に上回ります。一方、レンダリングにそれほど高いコストがかからず、CPU で実行できる場合は、ハードウェア アクセラレータを選ばなくても、最終的に、パフォーマンスが向上することがわかります。このような選択は容易ではありません。GPU の中には専用のメモリを備えておらず、システム メモリの一部を使用するものもあります。このような場合は、コピーのコストは少なくなります。
GDI でも WPF でも選択の余地がないことが問題です。GDI では、CPU を使用するしかありません。WPF では、どのようなレンダリング方法を使用しても、通常、Direct3D のハードウェア アクセラレーションの使用を余儀なくされます。
そこに登場したのが Direct2D です。
Direct2D と GDI/DC
Direct2D は、選択する対象が何であってもレンダリングを行うように設計されました。対象がウィンドウや Direct3D テクスチャであれば、Direct2D は、コピーを伴うことなく、GPU に直接レンダリングします。対象が Windows Imaging Component (WIC) ビットマップであれば、CPU を代わりに使用して、同じように直接レンダリングします。WPF は GPU への多くのレンダリングを試み、レンダリングできない場合はソフトウェア ラスタライザーを使用します。それに対して Direct2D は、ハードウェア アクセラレーションを利用できる場合は GPU の優れたイミディエイト モード レンダリングを使用し、GPU が使用できない場合や使用が望ましくない場合は、CPU に最も適したレンダリングを行うことによって、両方のメリットを活かします。
ご想像どおり、Direct2D を使用してレイヤード ウィンドウをレンダリングする方法は多数あります。いくつかの方法を見てみましょう。ここでは、ハードウェア アクセラレーションを使用するかどうかに応じて、それぞれ推奨アプローチを紹介します。
まず、図 3 の GDI+ Graphics クラスを切り取り、Direct2D DC レンダー ターゲットに置き換えます。これは、GDI に多くの投資を行ったアプリケーションがある場合は役立ちますが、最も効果的な解決策というわけではありません。Direct2D は、DC に直接レンダリングするのではなく、最初に内部の WIC ビットマップにレンダリングした後、その結果を DC にコピーします。これは、GDI+ よりも処理時間が短くなりますが、それでも、レンダリングに DC を使用しなければ必要のない余分なコピーが伴います。
この方法を使用するには、まず、D2D1_RENDER_TARGET_PROPERTIES 構造体を初期化します。これにより、Direct2D には、そのレンダー ターゲットに使用するビットマップの形式が指示されます。これはプリマルチプライ済み BGRA ピクセル形式だったことを思い出してください。これは、D2D1_PIXEL_FORMAT 構造体で表現され、次のように定義できます。
const D2D1_PIXEL_FORMAT format =
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM,
D2D1_ALPHA_MODE_PREMULTIPLIED);
const D2D1_RENDER_TARGET_PROPERTIES properties =
D2D1::RenderTargetProperties(
D2D1_RENDER_TARGET_TYPE_DEFAULT,
format);
これで、Direct2D ファクトリ オブジェクトを使用して、DC レンダー ターゲットを作成できるようになりました。
CComPtr<ID2D1DCRenderTarget> target;
Verify(factory->CreateDCRenderTarget(
&properties,
&target));
最後に、レンダー ターゲットに対して、その描画コマンドの送信先となる DC を指示する必要があります。
const RECT rect = {0, 0, bitmap.GetWidth(), bitmap.GetHeight()};
Verify(target->BindDC(bitmap.GetDC(), &rect));
これで、BeginDraw メソッド呼び出しと EndDraw メソッド呼び出しの間で通常どおりに Direct2D を使って描画した後、これまでのように、ビットマップの DC を使用して Update メソッドを呼び出すことができます。EndDraw メソッドでは、すべての描画がバインドされた DC にフラッシュされるようにします。
Direct2D と WIC
GDI DC をまったく使用しないで、WIC ビットマップを直接使用できるようになると、ハードウェア アクセラレーションを使用しなくても、最高のパフォーマンスを実現することができます。この方法を使用するには、まず、WIC を使用して、直接、プリマルチプライ処理済みの BGRA ビットマップを作成します。
CComPtr<IWICImagingFactory> factory;
Verify(factory.CoCreateInstance(
CLSID_WICImagingFactory));
CComPtr<IWICBitmap> bitmap;
Verify(factory->CreateBitmap(
m_info.GetWidth(),
m_info.GetHeight(),
GUID_WICPixelFormat32bppPBGRA,
WICBitmapCacheOnLoad,
&bitmap));
次に、以前と同じように、D2D1_RENDER_TARGET_PROPERTIES 構造体を初期化する必要があります。ただし、レンダー ターゲットと GDI に互換性が必要であることを Direct2D に指示することも必要です。
const D2D1_PIXEL_FORMAT format =
D2D1::PixelFormat(
DXGI_FORMAT_B8G8R8A8_UNORM,
D2D1_ALPHA_MODE_PREMULTIPLIED);
const D2D1_RENDER_TARGET_PROPERTIES properties =
D2D1::RenderTargetProperties(
D2D1_RENDER_TARGET_TYPE_DEFAULT,
format,
0.0f, // default dpi
0.0f, // default dpi
D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE);
これで、Direct2D ファクトリ オブジェクトを使用して、WIC レンダー ターゲットを作成できるようになりました。
CComPtr<ID2D1RenderTarget> target;
Verify(factory->CreateWicBitmapRenderTarget(
bitmap,
properties,
&target));
ところで、D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE は正確にはどのような処理を行っているのでしょう。これは ID2D1GdiInteropRenderTarget インターフェイスにレンダー ターゲットを照会することを Direct2D に示唆しています。
CComPtr<ID2D1GdiInteropRenderTarget> interopTarget;
Verify(target.QueryInterface(&interopTarget));
実装の簡素化と効率化のため、このインターフェイスの照会は常に成功します。ただし、前もって目的のインターフェイスを指定しなかった場合に使用を試みたときのみ、失敗します。
ID2D1GdiInteropRenderTarget インターフェイスには、GetDC と ReleaseDC という 2 つのメソッドしかありません。ハードウェア アクセラレーションを使用するケースを最大限に活用するために、これらのメソッドの使用は、ターゲットの BeginDraw メソッドと EndDraw メソッドの呼び出しの間に制限されます。GetDC は、DC を返す前にレンダー ターゲットをフラッシュします。相互運用インターフェイスのメソッドは対になっている必要があるため、図 7 に示すように、C++ クラスでラップすることが理にかなっています。
図 7 レンダー ターゲットの DC ラッパー クラス
class RenderTargetDC {
ID2D1GdiInteropRenderTarget* m_renderTarget;
HDC m_dc;
public:
RenderTargetDC(ID2D1GdiInteropRenderTarget* renderTarget) :
m_renderTarget(renderTarget),
m_dc(0) {
Verify(m_renderTarget->GetDC(
D2D1_DC_INITIALIZE_MODE_COPY,
&m_dc));
}
~RenderTargetDC() {
RECT rect = {};
m_renderTarget->ReleaseDC(&rect);
}
operator HDC() const {
return m_dc;
}
};
これで、ウィンドウの Render メソッドは、図 8 に示すように、RenderTargetDC を使用するよう更新できます。この方法の優れている点は、WIC レンダー ターゲットの作成に固有のすべてのコードが CreateDeviceResources メソッドに収容されていることです。次に、Direct3D レンダー ターゲットを作成してハードウェア アクセラレーションを使用する方法について説明しますが、いずれにしても、図 8 の Render メソッドはそのまま変わりません。これにより、アプリケーションは、描画コードをまったく変更することなく、レンダー ターゲットの実装を非常に簡単に切り替えることができるようになります。
図 8 GDI と互換性のある Render メソッド
void Render() {
CreateDeviceResources();
m_target->BeginDraw();
// Do some drawing here
{
RenderTargetDC dc(m_interopTarget);
m_info.Update(m_hWnd, dc);
}
const HRESULT hr = m_target->EndDraw();
if (D2DERR_RECREATE_TARGET == hr) {
DiscardDeviceResources();
}
else {
Verify(hr);
}
}
Direct2D と Direct3D/DXGI
ハードウェア アクセラレータを使用したレンダリングを行うには、Direct3D を使用する必要があります。自動的にハードウェア アクセラレーションを使用することになる ID2D1HwndRenderTarget を使って HWND に直接レンダリングしていないため、自身で Direct3D デバイスを作成し、GDI 互換の結果が得られるように、基になる DirectX Graphics Infrastructure (DXGI) の全体像を把握する必要があります。
DXGI は、Direct3D より下のレイヤーに存在する比較的新しいサブシステムで、基になるハードウェアから Direct3D を抽象化し、相互運用性のシナリオに対して高いパフォーマンスのゲートウェイを提供します。また、Direct2D は、この新しい API を利用して、Direct3D の今後のバージョンへの移行を簡単にします。この方法を使用するには、まず、Direct3D ハードウェア デバイスを作成します。これは、レンダリングを実行する GPU を表すデバイスです。ここでは、Direct2D で現在必要とされているため、Direct3D 10.1 API を使用しています。
CComPtr<ID3D10Device1> device;
Verify(D3D10CreateDevice1(
0, // adapter
D3D10_DRIVER_TYPE_HARDWARE,
0, // reserved
D3D10_CREATE_DEVICE_BGRA_SUPPORT,
D3D10_FEATURE_LEVEL_10_0,
D3D10_1_SDK_VERSION,
&device));
D3D10_CREATE_DEVICE_BGRA_SUPPORT フラグは Direct2D 相互運用性にとって非常に重要です。BGRA ピクセル形式はそろそろ見慣れてきたでしょう。従来の Direct3D アプリケーションでは、スワップ チェーンを作成し、レンダリングするテクスチャとしてそのバック バッファーを取得した後、レンダリングされたウィンドウを表示します。Direct3D をレンダリングのみに使用して表示には使用しないため、テクスチャ リソースを直接作成できます。テクスチャは、テクセルを格納するための Direct3D リソースです。テクセルは、Direct3D のピクセルに相当します。Direct3D には 1D、2D、および 3D のテクスチャが用意されていますが、必要なのは 2 D テクスチャだけです。これは、2D サーフェイスに密接にマップされます (図 9 参照)。
図 9 2D テクスチャ
D3D10_TEXTURE2D_DESC description = {};
description.ArraySize = 1;
description.BindFlags =
D3D10_BIND_RENDER_TARGET;
description.Format =
DXGI_FORMAT_B8G8R8A8_UNORM;
description.Width = GetWidth();
description.Height = GetHeight();
description.MipLevels = 1;
description.SampleDesc.Count = 1;
description.MiscFlags =
D3D10_RESOURCE_MISC_GDI_COMPATIBLE;
CComPtr<ID3D10Texture2D> texture;
Verify(device->CreateTexture2D(
&description,
0, // no initial data
&texture));
D3D10_TEXTURE2D_DESC 構造体では、作成するテクスチャを記述します。D3D10_BIND_RENDER_TARGET 定数は、テクスチャが Direct3D パイプラインの出力バッファー、つまりレンダー ターゲットとしてバインドされることを示します。DXGI_FORMAT_B8G8R8A8_UNORM 定数により、Direct3D は GDI に適切なピクセル形式を生成するようになります。最後に、D3D10_RESOURCE_MISC_GDI_COMPATIBLE 定数は、基になる DXGI サーフェイスに対して、レンダリングの結果を取得できる GDI DC を提供するように指示します。この Direct2D は、前のセクションで説明した ID2D1GdiInteropRenderTarget インターフェイスを通じて公開されます。
既に説明したように、Direct2D は、DXGI API を使用して Direct3D サーフェイスにレンダリングし、この API を Direct3D の特定のバージョンに関連付けることを回避できます。これは、Direct2D に渡す、Direct3D テクスチャの基になる DXGI サーフェイス インターフェイスを取得する必要があることを意味します。
CComPtr<IDXGISurface> surface;
Verify(texture.QueryInterface(&surface));
この時点で、Direct2D ファクトリ オブジェクトを使用して、DXGI サーフェイスのレンダー ターゲットを作成できます。
CComPtr<ID2D1RenderTarget> target;
Verify(factory->CreateDxgiSurfaceRenderTarget(
surface,
&properties,
&target));
レンダー ターゲットのプロパティは、前のセクションで説明したプロパティと同じです。適切なピクセル形式を使用して GDI 互換を要求することを忘れないでください。その後、ID2D1GdiInteropRenderTarget インターフェイスを照会し、図 8 と同じ Render メソッドを使用することができます。
これで完了です。ハードウェア アクセラレーションを使用してレイヤード ウィンドウをレンダリングする場合は、Direct3D テクスチャを使用してください。それ以外の場合は、WIC ビットマップを使用してください。この 2 つの方法を使用すると、コピー回数を最小限に抑えて、最高のパフォーマンスを実現できます。
DirectX のブログ (blogs.msdn.com/directx) (英語)、特に Ben Constable が 2009 年 8 月に投稿したコンポーネント化と相互運用性に関する記事をぜひご覧ください。
Kenny Kerr は、Windows 向けのソフトウェア開発を専門にしているソフトウェア設計者です。また、Window Clippings (windowclippings.com) の作成者でもあります。連絡先は weblogs.asp.net/kennykerr (英語) です。
ご質問やご意見は mmwincpp@microsoft.com (英語のみ) までお送りください。
この記事のレビューに協力してくれた技術スタッフの Ben Constable と Mark Lawrence に心より感謝いたします。