June 2009

Volume 24 Number 06

Windows と C++ - Direct2D の紹介

Kenny Kerr | June 2009

このコラムは、Windows 7 のプレリリース版を基にしています。ここに記載されている情報は変更される可能性があります。

目次

アーキテクチャとプリンシパル
ファクトリとリソース
レンダー ターゲット
ブラシと描画コマンド

数年前の Windows Vista の登場によって、Windows グラフィックス デバイス インターフェイス (GDI) の時代がもうすぐ終わることが明らかになりました。GDI は、GDI+ で補強したとしても、Windows Presentation Foundation (WPF) の優れたグラフィックス機能と比較すると時代の古さが現れていました。GDI では、ハードウェア アクセラレーションを使用しても機能不足だと考えられたためか、ハードウェア アクセラレーションがなくなりましたが、WPF では、Direct3D の豊富なハードウェア アクセラレーションを利用していました。

それでも、高パフォーマンス、高品質の市販アプリケーションを開発するには、依然として C++ とネイティブ コードの機能が頼りになります。そのため、個人的には、Direct2D と DirectWrite を導入するよりも Windows 7 のいくつかの機能を使用することに興味を惹かれます。Direct2D は、特に要件の高い、リッチなビジュアル デスクトップ アプリケーションで最大限の高パフォーマンスをサポートする新しい 2D グラフィックス API です。DirectWrite も Direct2D を補完する新しい API であり、Direct2D と併用することでハードウェア アクセラレータを使用したテキスト処理を行い、OpenType 文字体裁と ClearType テキスト レンダリングを高度にサポートした高品質のテキスト レイアウトとレンダリングを提供します。

この記事では、これらの新しいテクノロジについて説明し、その重要性と利用方法を示します。

アーキテクチャとプリンシパル

Windows XP では、オペレーティング システムに関しては GDI と Direct3D が対等の立場にあります。GDI と Direct3D のユーザー モードの部分は、カーネル モードの部分と直接インターフェイスをとります。そのため、両者とも、適切なディスプレイ ドライバがあれば、ハードウェアを直接高速化できます。Windows Vista では、ビデオ ハードウェアの制御は Direct3D だけに委ねられています。GDI は、突如として、Direct3D 上で主にソフトウェア ベースのレンダリングによってサポートされるレガシ グラフィックス API の仲間入りをすることになりました。

GDI は廃れ、今後は Direct3D の時代になりますが、これによって開発環境はどのように変わり、Direct2D および WPF にどのような影響がもたらされるのでしょうか。Direct3D はグラフィックスのレンダリングや GPU の動作の処理能力を増強する Windows のグラフィックス処理ユニット (GPU) API です。OpenGL (現在もサポートされています) を除くと、Direct3D は、ユーザー モード アプリケーションから直接グラフィックス ディスプレイ アダプタを制御する最も低レベルのグラフィックス パイプラインです。

Direct3D はイミディエイト モードのグラフィックス API と呼ばれます。これは単に API がグラフィックス ハードウェアの上位の薄い層であることを意味します。この API はハードウェアの機能へのアクセスを提供し、ハードウェア機能がなんらかの理由で制限される場合にその空白を埋める役割を果たします。開発者は、ディスプレイをレンダリングまたは更新するたびに、これからレンダリングを開始することを Direct3D に通知し、フレームのレンダリングに必要なすべての情報を Direct3D パイプラインに渡して、操作が完了したことを通知する必要があります。これによってディスプレイが更新されます。Direct3D には数多くの高度な 3D 関数がありますが、これらの関数を直接制御する作業は開発者に委ねられ、用意されている 3D プリミティブの数はあまり多くありません。言うまでもなく、これは容易な作業ではありません。

Windows Vista が登場する前は、Direct3D には、イミディエイト モードの API の上位に作成された保持モードのグラフィックス API も含まれていました。この API は、シーン (オブジェクトとライティングが設定されたフレームの階層) と呼ばれる 3D オブジェクトの操作を直接サポートしていました。保持モードと呼ばれるのは、API がシーン グラフ全体のコピーを保持するためです。アプリケーションがシーンを更新するだけで、API は自動的にディスプレイのレンダリングおよび更新を処理します。問題は、当然のことながら、この処理のすべてにコストが伴うということです。コストが高すぎる場合や、抽象型が要件を十分に満たしていない場合は、この方法をきっぱりあきらめ、イミディエイト モードの API を直接使用して固有のジオメトリ アルゴリズムを作成する必要があります。

WPF に詳しい読者は、前述した保持モードのグラフィックス API についてもよくご存知のことでしょう。Direct3D では、保持モードの API は廃止されましたが、WPF には、Media Integration Layer (MIL) と呼ばれる固有の内部保持モード API があります。MIL は WPF と Direct3D の中間に存在し、WPF によって定義されたシーン グラフのコピーを保持します。これによって、マネージ コードがバックグラウンドでビジュアル オブジェクトを操作し、MIL と調整して、保持しているシーン グラフに変更を反映することができます。この機能を利用すると、開発者は WPF できわめて高度な対話型のエクスペリエンスを実現できるようになりますが、それには高コストが伴います。

そこで、Direct2D の出番が明確になります。Direct2D では、単純な、または複雑なジオメトリ、ビットマップ、テキストを Direct3D のイミディエイト モード グラフィックス API で直接作成してきわめて高いパフォーマンスを実現し、リッチなレンダリングをサポートします。これにより、アプリケーションの高品質なグラフィックス コンテンツが高パフォーマンス、低オーバーヘッドで生成されます。Direct3D を直接使用する場合よりはるかに単純な API を使用して、通常はごくわずかなオーバーヘッドで 2D コンテンツが生成されます。プリミティブ単位のアンチエイリアシングなどのごく限られた処理を除いて、開発者は Direct3D で Direct2D よりも高い性能を実現する必要に迫られています。Direct2D は、MIL や Quartz2DE などよりはるかに高パフォーマンスです。

それでもまだ不足だと言わんばかりに、さらに機能向上が進み、リモート デスクトップ プロトコル (RDP) によるリモート レンダリング、サーバー側レンダリングのための、またはハードウェアの不足を補うためのソフトウェア フォールバック、ClearType テキストのレンダリングのサポート、GDI と Direct3D とのきわめて優れた相互運用性などのさまざまな機能が追加されています。要するに、この WPF の "空域" は無駄ではないということです。

次に、コードの作成について説明します。重点をわかりやすくするために、例では、すべての定型的なウィンドウ作成コードの処理に Active Template Library (ATL) と Windows Template Library (WTL) を使用します。Direct2D はレンダリングを処理するツールであることを思い出してください。ウィンドウにレンダリングする場合は、そのウィンドウを管理する必要があります。マウス入力が必要な場合にも、通常どおりにウィンドウ メッセージに応答する必要があります。いかがでしょうか。図 1 に、この記事でこれから説明するウィンドウのスケルトンを示します。

図 1 ウィンドウのスケルトン

#define HR(_hr_expr) { hr = _hr_expr; if (FAILED(hr)) return hr; }

class Window : public CWindowImpl<Window, CWindow, CWinTraits<WS_OVERLAPPEDWINDOW>>

{

public:

    BEGIN_MSG_MAP(Window)

        MSG_WM_DESTROY(OnDestroy)

    END_MSG_MAP()



    HRESULT Create()

    {

        VERIFY(__super::Create(0)); // top-level

        VERIFY(SetWindowText(L"Direct2D Sample"));



        VERIFY(SetWindowPos(0, // z-order

                            100, // x

                            100, // y

                            600, // pixel width,

                            400, // pixel height,

                            SWP_NOZORDER | SWP_SHOWWINDOW));



        VERIFY(UpdateWindow());

        return S_OK;

    }

private:

    void OnDestroy()

    {

        ::PostQuitMessage(1);

    }

};

ファクトリとリソース

Direct3D、XmlLite、Direct2D などのさまざまな API では、簡易版の COM 仕様に従って、IUnknown から派生したインターフェイスでマネージ オブジェクトの有効期間を管理します。COM ランタイムを初期化する必要はなく、アパートメントやプロキシのことを意識する必要もありません。これはリソース管理を簡素化し、API およびアプリケーションが明確に定義された方法でオブジェクトを公開し、使用することができるようにするための単なる規則です。Direct2D が COM インターフェイスを使用しているからといって、開発者がこれらのインターフェイスの固有の実装を提供できるというわけではありません。特に定めのない限り、Direct2D は Direct2D 固有の実装のみを処理します。インターフェイス ポインタの管理には、この記事の例のように、ATL の CComPtr スマート ポインタ クラスを使用することをお勧めします。

すべての Direct2D アプリケーションでは、まずファクトリ オブジェクトを作成します。D2D1CreateFactory 関数は ID2D1Factory インターフェイスの実装を返します。

CComPtr<ID2D1Factory> factory;

HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory));

もちろん、これらの型を使用する前に、Direct2D のヘッダー ファイル d2d1.h をインクルードして、Direct2D のさまざまなインターフェイス、構造体、定数、および D2D1CreateFactory 関数を宣言する必要があります。D2D1CreateFactory 関数をインポートするには、d2d1.lib ファイルを使用します。ヘッダー ファイル d2d1helper.h にも、C++ から Direct2D を簡単に使用できるようにするための便利な関数やクラスが多数あります。個人的には、プロジェクトに Direct2D をインクルードする場合には、通常、プロジェクトのプリコンパイル済みヘッダーに次のディレクティブを追加しています。

#include <d2d1.h>

#include <d2d1helper.h>

#pragma comment(lib, "d2d1.lib")

前述したように、Direct2D は COM を使用しませんが、D2D1CreateFactory の最初のパラメータを見ると、そうは見えないかもしれません。D2D1_FACTORY_TYPE の列挙で定数 D2D1_FACTORY_TYPE_SINGLE_THREADED および D2D1_FACTORY_TYPE_MULTI_THREADED が定義されますが、これらの定数は COM アパートメントとは無関係であり、スレッド アフィニティを持ちません。D2D1_FACTORY_TYPE_SINGLE_THREADED 定数は、ファクトリ オブジェクトおよびファクトリのルートにあるすべてのオブジェクトは一度に 1 つのスレッドからしかアクセスできないということを指定します。オブジェクトにこの定数を指定すると、単一スレッドからのアクセスでパフォーマンスが高くなり、レンダリングの呼び出しとの間で不要なシリアル化が行われません。D2D1_FACTORY_TYPE_MULTI_THREADED 定数は、ファクトリ オブジェクトおよびファクトリのルートにあるすべてのオブジェクトに対して、同時に複数のスレッドからアクセスできるということを指定します。Direct2D は、インターロックされた参照カウントとの間で必要な同期を行います。この機能は、特定のリソースを共有し、別のターゲットに同時にレンダリングする場合に便利です。並列処理は CPU に固有であり、GPU に送信される命令はシリアル化されて、最終的に個別に並列処理される可能性もあります。

では、ファクトリ オブジェクトはどのような目的で使用するのでしょうか。ファクトリ オブジェクトは、デバイス非依存のすべてのリソース (主にジオメトリ) とデバイスを表すレンダー ターゲットの作成を処理します。レンダー ターゲットはデバイス依存のリソースの作成を処理します。デバイス依存リソースとデバイス非依存のリソースとの違いは、Direct2D を適切に使用するうえで重要です。WPF などの保持モードのグラフィックス API の長所の 1 つは、ディスプレイ デバイスが失われた場合の処理を、一般にハードウェアとプログラミング モデルとの中間層で行うということです。ディスプレイ デバイスが失われる理由は、解像度がバックグラウンドで変更された、ユーザーがラップトップのドッキングを解除したときなどにディスプレイ アダプタが削除された、リモート デスクトップの接続セッションが失われたなど、さまざまです。Direct2D のようなイミディエイト モードのグラフィックス API では、これらの場合を考慮する必要があります。もちろん、特定デバイスへのレンダリングに使用するリソースがそのデバイス上に物理的に存在するため、パフォーマンスが高くなるというメリットもあります。

これらの理由から、デバイス非依存のリソースとデバイス依存のリソースは明確に区別して作成することをお勧めします。これに対応するため、図 2 では、図 1 の Window クラスに 2 つのメソッドを追加しています。

図 2 デバイス非依存のリソースとデバイス依存のリソースを個別に作成する

HRESULT CreateDeviceIndependentResources()

{

    HRESULT hr;

    HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_factory));

    // TODO: Create device-independent resources here

    return S_OK;

}

HRESULT CreateDeviceResources()

{

    HRESULT hr;

    // TODO: Create device resources here

    return S_OK;

}

以降で具体的なコードを作成していきますが、デバイス非依存のリソースを作成する必要があるのは 1 度だけなので、当面は、Window クラスの Create メソッドから CreateDeviceIndependentResources を呼び出す方法でもかまいません。CreateDeviceResources の場合は、デバイス リソースを作成する必要があるときにオンデマンドで呼び出します。次のように、m_factory メンバ変数を追加する方法もあります。

CComPtr<ID2D1Factory> m_factory;

レンダー ターゲット

レンダー ターゲットは、デバイスを表す場合に使用し、基盤となるデバイスに依存します。レンダー ターゲットを使用して、ブラシなどのさまざまなリソースを作成し、描画操作を行うことができます。Direct2D は、さまざまな種類のレンダー ターゲットをサポートしています。Direct2D アプリケーションを一から作成する場合、レンダー ターゲットを作成し、コンテンツをウィンドウ (HWND) にレンダリングする方法があります。レンダー ターゲットを作成し、GDI のデバイス コンテキスト (DC) または DirectX Graphics Infrastructure (DXGI) のサーフェイスにレンダリングして、Direct3D アプリケーションで使用することもできます。さまざまな種類のビットマップ リソースのレンダー ターゲットを作成して、画面の表示領域外でレンダリングすることも可能です。

各種のレンダー ターゲットを作成するためのさまざまなファクトリ メソッドが用意されています。各メソッドにはわかりやすい名前が付いています。たとえば、CreateHwndRenderTarget メソッドはウィンドウのレンダー ターゲットを作成し、CreateDxgiSurfaceRenderTarget メソッドは DXGI サーフェイスのレンダー ターゲットを作成します。ウィンドウにレンダリングするには、CreateDeviceResources メソッドを更新して、ウィンドウのレンダー ターゲットを作成する必要があります。そのために必要な最低限のコードは図 3 のようになります。このサンプルは DPI 対応ではありませんが、これについての説明は別の機会に譲ることにします。

図 3 ウィンドウのレンダー ターゲットを作成する

HRESULT CreateDeviceResources()

{

    HRESULT hr;

    if (0 == m_target)

    {

        CRect rect;

        VERIFY(GetClientRect(&rect));

        D2D1_SIZE_U size = D2D1::SizeU(rect.Width(), rect.Height());

        HR(m_factory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),

                        D2D1::HwndRenderTargetProperties(m_hWnd, size), &m_target));

    }

    return S_OK;

}

次のように、m_target メンバ変数を追加する方法もあります。

CComPtr<ID2D1HwndRenderTarget> m_target;

CreateDeviceResources メソッドは、レンダー ターゲットが未作成の場合、または後で説明するように、デバイスが失われてレンダー ターゲットを再作成する必要がある場合にのみデバイス リソースを作成します。

CreateHwndRenderTarget メソッドの最初のパラメータは、D2D1_RENDER_TARGET_PROPERTIES 型です。このパラメータはすべてのレンダー ターゲット作成関数に共通です。RenderTargetProperties 関数は、d2d1helper.h ヘッダー ファイルに定義されたヘルパ関数です。これは、Direct2D で使用されるデータ構造体の多くを初期化する場合の共通のパターンであり、この共通パターンによってコードが大幅に簡素化されます。RenderTargetProperties ヘルパ関数には、結果構造体の最も一般的な初期化を考慮した既定値を持つパラメータがあります。これらの既定値をオーバーライドして、ピクセル形式や DPI 情報、およびレンダー ターゲットの追加の特性 (ハードウェア ベースまたはソフトウェア ベースのレンダリングを設定するかどうか、GDI の相互運用性をサポートするかどうかなど) を調整することができます。

同様に、CreateHwndRenderTarget の 2 番目のパラメータは D2D1_HWND_RENDER_TARGET_PROPERTIES 構造体であり、ウィンドウに固有の情報を指定します。

レンダー ターゲットの作成が完了したら、レンダリングを行います。それには、図 4 のような基本的な構造で Window クラスに Render メソッドを追加します。

図 4 レンダリング

void Render()

{

    if (SUCCEEDED(CreateDeviceResources()))

    {

        if (0 == (D2D1_WINDOW_STATE_OCCLUDED & m_target->CheckWindowState()))

        {

            m_target->BeginDraw();

            m_target->SetTransform(D2D1::Matrix3x2F::Identity());

            m_target->Clear(D2D1::ColorF(D2D1::ColorF::Red));

            // TODO: add drawing code here



            if (D2DERR_RECREATE_TARGET == m_target->EndDraw())

            {

                DiscardDeviceResources();

            }

        }

    }

}

この場合、デバイス リソースが再利用され、デバイスの消失を予期できることで効率的な更新が可能になるため、このロジックはきわめて重要です。Render メソッドを起動するには、前述した CreateDeviceResources メソッドを呼び出します。デバイス リソースが使用可能な状態であれば、何も処理する必要はありません。ここでは、最適化のために、ウィンドウの状態を確認して、ウィンドウの表示が妨げられていないことを確かめています。これは、ウィンドウが表示されない状態になっている場合に、描画しても貴重な CPU や GPU のリソースが無駄になるということがわかる便利な方法です。デスクトップ ウィンドウ マネージャ (DWM) でデスクトップ コンポジションを処理している場合には起こりにくい現象ですが、害はないので、デスクトップ コンポジションを無効にしている場合には有効な最適化方法です。

描画は、レンダー ターゲットの BeginDraw メソッドの呼び出しと EndDraw メソッドの呼び出しの間で行う必要があります。レンダー ターゲットの多くのメソッドの戻り値型は void です。描画操作はバッチ処理されるため、描画操作がフラッシュされ、デバイスにレンダリングされるまでエラーは検出されません。そのため、EndDraw メソッドに HRESULT 型の戻り値があります。EndDraw メソッドは、D2DERR_RECREATE_TARGET 定数を使用して、レンダー ターゲットが無効になり、再作成が必要であることを示します。次に、DiscardDeviceResources を呼び出して、すべてのデバイス リソースを解放します。

void DiscardDeviceResources()

{

    m_target.Release();

}

レンダー ターゲットの SetTransform メソッドと Clear メソッドの呼び出しについても少し説明しておきます。この場合の SetTransform は、以降の描画操作を単位行列で変換するということを指定しています。単位行列は変換ではないので、これは、描画操作でデバイス非依存のピクセルを単位としてリテラル座標空間を使用することを確認しているだけです。ただし、SetTransform をレンダリング時に繰り返し呼び出して、変換をその都度変更することができます。Clear メソッドは、後続の描画操作のために描画領域を単純にクリアし、この例の場合は色を赤に変更します。

この時点でウィンドウを作成すると、ウィンドウは白のままになります。Render メソッドを呼び出してディスプレイを更新するには、まだ必要な処理が残っています。そのためには、ウィンドウ メッセージ WM_PAINT および WM_DISPLAYCHANGE のメッセージ ハンドラを追加します。それには、次の 2 行をメッセージ マップに追加します。

MSG_WM_PAINT(OnPaint)

MSG_WM_DISPLAYCHANGE(OnDisplayChange)

WM_PAINT に応答することはもちろんですが、ディスプレイの解像度または色深度が変更されてもウィンドウが適切に再描画されるようにするためには、WM_DISPLAYCHANGE を処理することも同様に重要です (図 5 を参照)。

図 5 再描画

void OnPaint(CDCHandle /*dc*/)

{

    PAINTSTRUCT paint;

    VERIFY(BeginPaint(&paint));

    Render();

    EndPaint(&paint);

}

void OnDisplayChange(UINT /*bpp*/, CSize /*resolution*/)

{

    Render();

}

BeginPaint によって返された DC は使用されていないことに注意してください。ウィンドウを作成すると、目的どおり、赤の背景でウィンドウが表示されます。ところが、ウィンドウのサイズを変更すると、多少ちらつきが見られます。WM_ERASEBKGND メッセージの既定の処理が行われているため、ウィンドウの背景は相変わらずウィンドウ クラスの背景ブラシで自動的にクリアされています。これを防ぐには、メッセージを処理し、OnEraseBackground メソッドで TRUE を返して、ウィンドウの背景をクリアする処理を開発者が制御することを通知します。これで、ウィンドウのサイズを変更してもちらつきがなくなります。

ついでに、ウィンドウのサイズ変更を処理できると便利です。その場合は、次のようにして、WM_SIZE ウィンドウ メッセージのメッセージ ハンドラを追加します。

MSG_WM_SIZE(OnSize)

次に、OnSize ハンドルで、サイズが変更されたことをレンダー ターゲットに通知する必要があります。

void OnSize(UINT /*type*/, CSize size)

{

    if (0 != m_target)

    {

        if (FAILED(m_target->Resize(D2D1::SizeU(size.cx, size.cy))))

        {

            DiscardDeviceResources();

            VERIFY(Invalidate(FALSE));

        }

    }

}

サイズ変更が失敗した場合は、デバイス リソースを破棄すると、次回のウィンドウのレンダリング時に自動的に再作成されます。WM_SIZE メッセージはクライアント領域のサイズをピクセル単位で通知するため、Resize メソッドが新しいサイズをデバイスのピクセルで取得する方法に注意してください。Direct2D がデバイス非依存の座標系を使用しても、ウィンドウのレンダー ターゲットはそれを最終的にデバイスのピクセルにマップする必要があることを認識しています。

ブラシと描画コマンド

ブラシを作成すると、わかりやすい方法で描画できます。さまざまな幾何図形やテキストを描画するには、ブラシが必要です。ブラシはデバイス依存のリソースに分類されるので、ブラシの作成は CreateDeviceResources メソッドで行います。ご想像どおり、Direct2D には、ソリッドおよびビットマップ ブラシと、線形および放射状のグラデーション ブラシがあります。単色ブラシを作成するには、レンダー ターゲットを作成した後に、次のメソッドの呼び出しを追加します。このようにすると、レンダー ターゲットが再作成されたときにだけ単色ブラシが再作成されます。

HR(m_target->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &m_brush));

次のようにして、DiscardDeviceResources メソッドでブラシを解放することも忘れないでください。

m_brush.Release();

次のようにして、m_brush メンバ変数を追加することもできます。

CComPtr<ID2D1SolidColorBrush> m_brush;

これで、Clear メソッドと EndDraw メソッドの間で、Render メソッドに基本的な描画コマンドを追加できます。図 6 のコードは、対角線が引かれた円を生成します。ブラシの詳細については今後の記事のテーマとして残しておきますが、Direct2D を使用すると、レンダリング中にブラシの色を効率的に変更できるため、色ごとに異なる複数のブラシを作成する必要がないということだけはお伝えしておきます。

図 6 円と対角線

fig02.gif

図 7 基本的な描画

m_target->DrawLine(D2D1::Point2(10.0f, 10.0f), // start

                   D2D1::Point2(200.0f, 200.0f), // end

                   m_brush,

                   10.0f); // stroke width



const D2D1_POINT_2F center = D2D1::Point2(105.0f, 105.0f);



const D2D1_ELLIPSE ellipse = D2D1::Ellipse(center,

                                           95.0f, // radius X

                                           95.0f); // radius Y



m_target->DrawEllipse(&ellipse,

                      m_brush,

                      5.0f); // stroke width

前述したように、Direct2D はデバイス非依存の座標系を使用するため、表現された座標は、必ずしもディスプレイのピクセルにマップされるとは限らず、むしろターゲット デバイスの DPI の設定を使用します。この描画コマンドの結果を図 7 に示します。

ここで使用したのは不透明ブラシのみですが、ブラシおよびアルファ ブレンドの作成時にアルファ値を自由に指定することもできます。

スペースの都合で詳しく説明できませんが、ネイティブ アプリケーションで Direct2D と DirectWrite を活用する可能性についても妙案があります。リッチなジオメトリや操作、DirectWrite の各機能などの新しいテクノロジには、今後のコラムで紹介したいと思うさまざまな要因があります。その間に、Direct2D を使ってみてください。その魅力がわかるはずです。

インサイト: Direct2D のレンダリング

多くの人が、Direct2D アプリケーションと Direct3D アプリケーションのパフォーマンスを比較したいと思うでしょうが、これはりんごとみかんを比較するようなものです。Direct2D は一連のプリミティブを提供するものであり、比較対象としては他の 2D レンダリング API (GDI/GDI+ や WPF など) の方がはるかに適しています。使い方もはるかに単純です。さまざまな場面で、単純な Direct3D アプリケーションより Direct2D の方が高パフォーマンスであることは意外に思われるかもしれません。たとえば、色の異なる大量の線を描画する場合や、ビットマップから繰り返しブレンドする場合には、Direct2D の方が単純な Direct3D アプリケーションよりパフォーマンスが高くなります。

Direct2D でこの処理を行う場合に重要なことは、Direct2D と Direct2D に対して発行される描画要求との間、および Direct2D から Direct3D に対して発行されるプリミティブとの間に 1 対 1 の関係が維持されないということです。Direct2D は、Direct3D に対する要求をさまざまな方法で集約しようとします。

頂点バッファ マッピングの削減

単純な Direct3D アプリケーションの場合を考えてみましょう。このアプリケーションで、頂点バッファを作成し、そのバッファにジオメトリを書き込み、マップを解除して描画するとします。この方法の問題点は、次のマップが要求されたときに GPU がまだ頂点バッファを使用中の場合、GPU の処理が完了するまで CPU の速度は低下し、アプリケーション メモリにマップし直さざるを得ないということです。これにより、アプリケーションのパフォーマンスは大幅に低下します。

Direct2D は、頂点バッファをメモリにマップし、連続する多数のプリミティブのジオメトリをメモリに蓄積することで、この問題を解決しています。頂点バッファがいっぱいになったとき、あるいはアプリケーションが Flush または EndDraw を呼び出すときだけ、Direct2D は描画の呼び出しを Direct3D に送ります。これにより、マップおよびマップ解除の呼び出し回数が大幅に削減されるため、失速する GPU の数が少なくなります。

描画呼び出しの結合

ジオメトリを頂点バッファに蓄積できるようになっても、単純な Direct3D アプリケーションは多くの Direct2D の Draw 呼び出しの場合に、Direct3D に Draw の呼び出しを発行する必要があります。2 つの異なる長方形を異なる色でレンダリングし、色ごとに異なるシェーダ定数データを作成してそれぞれの色を表す必要があるとします。この場合、シェーダ定数データを更新するたびに、異なる Draw 呼び出しを発行する必要があります。

これは、その他の特定の定数が変更されないことを前提にした場合の話です。たとえば、同じ種類のブラシを連続して使用するとします。または、ビットマップ ブラシの場合、同じ入力ビットマップを一貫して使用します。Direct2D では、頂点シェーダを使用してシェーダ定数データを継続的に蓄積し、Direct3D の Draw を 1 回呼び出して、Direct2D の大量の Draw 呼び出しを処理することができます。

順序不定のテキスト レンダリング

Direct2D のテキスト レンダリング パイプライン全体がハードウェアで 3 段階に分けて実行されます。最初の段階では、テクスチャにグリフを書き込み、次の段階でこれらのグリフを低解像度処理し、最後の段階でフィルタリング操作を実行してテキストをレンダー ターゲットに変換します。単純なアプリケーションでは各段階を順番にレンダリングし、レンダー ターゲットを 3 回変更し、Draw の呼び出しを発行して各テクスチャ間でデータを移動します。Direct2D はテキストのプリミティブを他のプリミティブから不定の順序でレンダリングします。この場合、最初に大量のグリフを蓄積します。次に、そのグリフのすべてを低解像度処理し、最終的なグリフの集まりを Direct2D に発行された他の幾何プリミティブと順番にブレンドします。このような方法でテキスト レンダリングを結合すると、他のプリミティブが Direct2D テキスト レンダリングの呼び出しとインターリーブされる場合でも、テキストをレンダリングするためのレンダー ターゲットの変更回数および全般的な Direct3D のデバイス状態の変更回数が削減されます。

頂点バッファのバッチ処理、描画呼び出しの結合、順序不定のテキスト レンダリングを組み合わせることで、アプリケーションが直接 Direct3D をターゲットにした場合には膨大な開発作業を必要とするレベルのレンダリング パフォーマンスを Direct2D で実現することができます。

--Mark Lawrence、マイクロソフト シニア ソフトウェア開発エンジニア

ご意見やご質問は mmwincpp@microsoft.com まで英語でお送りください。

Kenny Kerr は、Windows 向けのソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は weblogs.asp.net/kennykerr です。