DirectX

Windows ストア アプリで XAML、DirectX、および C++ を使用する

Doug Erickson

 

Windows Vista 以降、DirectX が Windows プラットフォーム向けの主要グラフィックス API となり、グラフィックス処理装置 (GPU) によるアクセラレーションが OS 画面描画のすべての操作に有効になります。ただし Windows 8 がリリースされるまで、DirectX の開発者はネイティブ C++ と COM で独自の UI フレームワークを新しく作成するか、Scaleform などのミドルウェア UI パッケージのライセンスを利用する必要がありました。

Windows 8 では、Windows ランタイム (WinRT) の DirectX-XAML 相互運用機能を使用して、ネイティブ DirectX と適切な UI フレームワークのギャップを埋めることができます。DirectX で XAML の API サポートを利用するには、"ネイティブ" C++ を使用する必要があります (ただし、スマート ポインターと C++ コンポーネントの拡張機能は利用できます)。今回は COM についての基本的な知識が少しあれば役に立ちますが、XAML フレームワークと DirectX を連携させて操作する必要がある具体的な相互運用について詳しく説明します。

この 2 部構成のコラムでは、DirectX-XAML 相互運用への 2 つのアプローチについて見ていきます。1 つは、DirectX グラフィックス API を使用して XAML フレームワーク要素にサーフェスを描画するアプローチ、もう 1 つは、XAML コントロール階層を DirectX スワップ チェイン サーフェスの上位に描画するアプローチです。

今回は最初のアプローチを取り上げ、XAML フレームワーク内に表示されるイメージまたはプリミティブをレンダリングするシナリオについて説明します。

しかし、その前に API のオプションの概要を簡単に説明します。現時点で Windows ランタイムには、DirectX 相互運用をサポートする 3 つの XAML 型があります。

  • Windows::UI::Xaml::Media::Imaging::SurfaceImageSource (以後 SurfaceImageSource): この型は、DirectX グラフィックス API を使用して共有 XAML サーフェスに比較的静的なコンテンツを描画できるようにします。ビュー全体が WinRT XAML フレームワークによって管理され、すべてのプレゼンテーション要素が WinRT XAML フレームワークによって同様に管理されます。これは、複雑なコンテンツでも各フレームが変化しない場合には理想的ですが、高い頻度で更新が行われる複雑な 2D ゲームや 3D ゲームには適していません。
  • Windows::UI::Xaml::Media::Imaging::VirtualSurfaceImageSource (以後 VirtualSurfaceImageSource): SurfaceImageSource と同様、XAML フレームワーク向けに定義されたグラフィックス リソースを使用します。SurfaceImageSource とは異なり、VirtualSurfaceImageSource では、更新と更新の間で変化するサーフェス領域だけを DirectX が描画する最適化された領域ベースの方法で、論理的に大きなサーフェスをサポートします。たとえば、マップ コントロールや大きな画像が密集するドキュメント ビューアーを作成する場合はこの要素を選択します。この型も SurfaceImageSource と同様、特にリアルタイムの表示やフィードバックを利用する複雑な 2D ゲームや 3D のゲームには適していません。
  • Windows::UI::Xaml::Controls::SwapChainBackgroundPanel (以後 SwapChainBackgroundPanel): この型の XAML コントロール要素を使用するアプリでは、XAML 要素を描画できる場所でカスタム DirectX ビュー プロバイダー (およびスワップ チェーン) を利用でき、たとえば最新ゲームのように、かなり待機時間が短いプレゼンテーションや高頻度のフィードバックを必要とするシナリオでパフォーマンスが向上します。アプリは、XAML フレームワークとは別に SwapChainBackgroundPanel の DirectX デバイス コンテキストを管理します。そのため、当然、更新時には SwapChainBackgroundPanel と XAML フレームの両方が相互に同期されません。バックグラウンド スレッドから SwapChainBackgroundPanel にレンダリングすることもできます。

今回は、SurfaceImageSource API と Virtual­SurfaceImageSource API について確認し、XAML の豊富なイメージ コントロールやメディア コントロールに API を組み込む方法を説明します (SwapChainBackgroundPanel は特殊なので、別に独自のコラムを用意します)。

注: SurfaceImageSource と VirtualSurfaceImageSource は C# または Visual Basic .NET から使用できますが、DirectX レンダリング コンポーネントは C++ で記述して、C# プロジェクトからアクセスする別の DLL にコンパイルする必要があります。SurfaceImageSource や VirtualSurfaceImageSource の代わりに使用できる、SharpDX (sharpdx.org)、MonoGame (monogame.net) などサードパーティが管理する WinRT DirectX フレームワークもあります。

では、始めましょう。今回は DirectX、具体的には Direct2D、Direct3D、および Microsoft DirectX Graphics Infrastructure (DXGI) の基礎を理解していることを前提としています。もちろん、XAML と C++ も理解している必要があり、中級 Windows アプリ開発者を対象とします。それでは、気を引き締めて先に進みましょう。

SurfaceImageSource および DirectX イメージ コンポジション

Windows::UI::Xaml::Media::Imaging 名前空間には、XAML の他の多くのイメージング型と共に SurfaceImageSource 型が含まれています。事実、SurfaceImageSource 型では、多くの XAML グラフィックスやイメージング プリミティブを共有サーフェスに動的に描画する方法が提供されます。DirectX グラフィックス呼び出しを使ってレンダリングするコンテンツを効果的に塗りつぶし、ブラシとして適用します (具体的には、ImageBrush として使用するのは ImageSource です)。これは DirectX を使用して実行時に生成するビットマップと考えられ、この型はビットマップなどのイメージ リソースを適用できる多くの場所に使用できます。

今回の目的としては、プレースホルダーとして空の PNG イメージを含む <Image> XAML 要素に描画します。<Image> 要素に高さと幅を指定し、この情報をコードで SurfaceImageSource コンストラクターに渡します (高さと幅を指定しないと、レンダリングするコンテンツはこの <Image> タグ パラメーターに合うように引き伸ばされます)。

<Image x:Name="MyDxImage" Width="300" Height="200" Source="blank-image.png" />

今回の例のターゲットは <Image> タグで、描画先にサーフェスを表示します。<Rectangle>、<Ellipse> などの XAML プリミティブも同様に使用でき、どちらも SurfaceImageSource ブラシで塗りつぶすことができます。これらのプリミティブやイメージの描画は Windows ランタイムによって DirectX を使用して実行されるため、これが可能になります。ここで行っているすべてのことは、内部でさまざまなレンダリング ソースをフックすることです。

コードでは、次のものをインクルードします。

#include <wrl.h>
#include <wrl\client.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include <d2d1_1.h>
#include <d3d11_1.h>
#include "windows.ui.xaml.media.dxinterop.h"

これらは Windows Runtime Library (WRL) のヘッダーで、重要な DirectX コンポーネントの一部であり、最も重要なことはネイティブ DirectX 相互運用インターフェイスを含むことです。後で必要になるものをインクルードしていますが、すぐに明らかにしていきます。

dxgi.lib、d2d1.lib、d3d11.lib などの対応するライブラリもインポートします。

便宜上、次の名前空間もインクルードします。

using namespace Platform;
using namespace Microsoft::WRL;
using namespace Windows::UI::Xaml::Media;
using namespace Windows::UI::Xaml::Media::Imaging;

コードでは、MyImageSourceType という型を作成します。この型は SurfaceImageSource 基本型から継承し、基本型のコンストラクターを呼び出します (図 1 参照)。

図 1 SurfaceImageSource から派生

public ref class MyImageSourceType sealed : Windows::UI::Xaml::
  Media::Imaging::SurfaceImageSource
{
  // ...
  MyImageSourceType::MyImageSourceType(
    int pixelWidth,
    int pixelHeight,
    bool isOpaque
  ) : SurfaceImageSource(pixelWidth, pixelHeight, isOpaque)
  {
    // Global variable that contains the width,
    // in pixels, of the SurfaceImageSource.
    m_width = pixelWidth;
    // Global variable that contains the height, 
    // in pixels, of the SurfaceImageSource.
    m_height = pixelHeight;
    CreateDeviceIndependentResources();
    CreateDeviceResources();
  }
  // ...
}

注: SurfaceImageSource から継承する必要はありませんが、コードの整理という点で少し簡単になります。SurfaceImageSource オブジェクトのインスタンスをメンバーとして作成して使用するだけです。コード例のオブジェクトの自己参照 ("this") を頭の中でメンバーの名前に置き換えてください。

CreateDeviceResources メソッドと CreateDeviceIndependentResources メソッドは、DirectX グラフィックス ハードウェア インターフェイス固有の設定と、より一般的な DirectX アプリ固有の設定を論理的に分離するための便利な方法となるユーザー実装です。両方のメソッドで行われる動作は不可欠です。ただし、デバイスに依存しないリソースに影響を与えることなくデバイス固有のリソースを作り直すことも、その逆も必要になるため、これらを分離することは適切な (必要な) 設計です。

CreateDeviceResources のコードは、少なくとも基本的な形として 図 2 のようにします。

図 2 DirectX のデバイス固有のリソースの作成

// Somewhere in a header you have defined the following:
Microsoft::WRL::ComPtr<ISurfaceImageSourceNative> m_sisNative;
// Direct3D device.
Microsoft::WRL::ComPtr<ID3D11Device> m_d3dDevice;
// Direct2D objects.
Microsoft::WRL::ComPtr<ID2D1Device> m_d2dDevice;
Microsoft::WRL::ComPtr<ID2D1DeviceContext> m_d2dContext;
// ...
void MyImageSourceType::CreateDeviceResources()
{
  // This flag adds support for surfaces with a different color channel ordering
  // from the API default. It’s required for compatibility with Direct2D.
  UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if defined(_DEBUG)   
  // If the project is in a debug build, enable debugging via SDK Layers.
  creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
  // This array defines the set of DirectX hardware feature levels this
  // app will support. Note the ordering should be preserved.
  // Don't forget to declare your application's minimum required
  // feature level in its description. All applications are assumed
  // to support 9.1 unless otherwise stated.
  const D3D_FEATURE_LEVEL featureLevels[] =
  {
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_9_3,
    D3D_FEATURE_LEVEL_9_2,
    D3D_FEATURE_LEVEL_9_1,
  };
  // Create the Direct3D 11 API device object.
  D3D11CreateDevice(
    nullptr,                       
    D3D_DRIVER_TYPE_HARDWARE,
    nullptr,
    creationFlags,                 
    featureLevels,                 
    ARRAYSIZE(featureLevels),
    // Set this to D3D_SDK_VERSION for Windows Store apps.
    D3D11_SDK_VERSION,
    // Returns the Direct3D device created in a global var.
    &m_d3dDevice,                  
      nullptr,
      nullptr);
    // Get the Direct3D API device.
    ComPtr<IDXGIDevice> dxgiDevice;
    m_d3dDevice.As(&dxgiDevice);
    // Create the Direct2D device object and a
    // corresponding device context.
    D2D1CreateDevice(
      dxgiDevice.Get(),
      nullptr,
      &m_d2dDevice);
    m_d2dDevice->CreateDeviceContext(
      D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
      &m_d2dContext);
    // Associate the DXGI device with the SurfaceImageSource.
    m_sisNative->SetDevice(dxgiDevice.Get());
}

ここで、ハードウェア デバイス コンテキストを作成して結び付けます。ちょっと待ってください。ISurfaceImageSourceNative とは何でしょう。これは WinRT 型ではありません。どのような処理が行われるのでしょう。

これが相互運用の部分です。ここが WRL の "Jeffries チューブ" に潜入して少し調整する部分です。また、多くの WRL の背後にある COM に作用する部分でもあります。

相互運用動作を有効にするには、基本的に内部で DirectX ソースにプラグインする必要があります。そのためには、WRL 固有の COM インターフェイスの ISurfaceImageSourceNative で定義されるメソッドの実装に今回の型をフックする必要があります。これが完了したら、この型を <Image> 要素 (今回の例) にアタッチし、アプリが更新を XAML フレームワークにプッシュするとき、既定の描画呼び出しの代わりに今回の DirectX 実装の描画呼び出しを使用します。

ISurfaceImageSourceNative は前述の相互運用ヘッダーで定義されています。では何が行われるか見てみましょう。

ここで、アプリ固有の CreateDeviceIndependentResources メソッドでは、COM を抜け出し、SurfaceImageSource で定義されているネイティブ メソッドにクエリします。これらのメソッドは直接公開されていないため、SurfaceImageSource または SurfaceImageSource 派生型の IUnknown::QueryInterface への呼び出しを使用して取得する必要があります。そのためには、SurfaceImageSource 派生型をすべての COM インターフェイスの基本インターフェイスである IUnknown として再キャストします (すべての WinRT 型の "基本" インターフェイスである IInspectable としてキャストすることもできます。IInspectable は IUnknown から継承されます)。その後、ISurfaceImageSourceNative メソッドのリストを取得するために、次のようにこのインターフェイスへのクエリを実行します。

void MyImageSourceType::CreateDeviceIndependentResources()
{
  // Query for ISurfaceImageSourceNative interface.
  reinterpret_cast<IUnknown*>(this)->QueryInterface(
    IID_PPV_ARGS(&m_sisNative));
}

(IID_PPV_ARGS はインターフェイス ポインターを取得する WRL のとても便利なヘルパー マクロです。SurfaceImageSource から継承していない場合、this を SurfaceImageSource オブジェクトのメンバー名に置き換えます)。

最後に、CreateDeviceResources メソッドの次の部分に意味があります。

m_sisNative->SetDevice(dxgiDevice.Get());

ISurfaceImageSourceNative::SetDevice は構成済みのグラフィックス インターフェイスを受け取り、これをすべての描画操作用にサーフェスに接続します。ただし、これは最低でも事前に 1 度 CreateDeviceIndependentResources 呼び出し後に CreateDeviceResources を呼び出す必要があることを意味します。そうしなければ構成済みのデバイスをアタッチできません。

これで、MyImageSourceType 型の派生元の SurfaceImageSource 型の基盤となる ISurfaceImageSourceNative 実装を公開できるようになります。描画呼び出しの基本実装であり、独自の呼び出しではないにもかかわらず、SurfaceImageSource 型への接続を事実上内部から取り出し移動しました。ここで、独自の呼び出しを実装します。

そのためには、次のメソッドを実装します。

  • BeginDraw: 描画用にデバイス コンテキストを開きます。
  • EndDraw: デバイス コンテキストを閉じます。

注: ISurfaceImageSourceNative メソッドとの結び付きをある程度緩やかにするために、BeginDraw と EndDraw というメソッド名にしました。このパターンは利便性が目的で、強制ではありません。

今回の BeginDraw メソッド (または派生型で定義した他の描画初期化メソッド) は、どこかの時点で、ISurfaceImageSourceNative::BeginDraw を呼び出す必要があります (最適化のために、更新するイメージ領域を指定するサブ長方形のパラメーターを追加できます)。同様に、EndDraw メソッドでは ISurfaceImageSourceNative::EndDraw を呼び出します。

この場合の BeginDraw メソッドと EndDraw メソッドは、図 3 のようなコードになります。

図 3 DirectX サーフェスへの描画

void MyImageSourceType::BeginDraw(Windows::Foundation::Rect updateRect)
{   
  POINT offset;
  ComPtr<IDXGISurface> surface;
  // Express target area as a native RECT type.
  RECT updateRectNative;
  updateRectNative.left = static_cast<LONG>(updateRect.Left);
  updateRectNative.top = static_cast<LONG>(updateRect.Top);
  updateRectNative.right = static_cast<LONG>(updateRect.Right);
  updateRectNative.bottom = static_cast<LONG>(updateRect.Bottom);
  // Begin drawing - returns a target surface and an offset
  // to use as the top-left origin when drawing.
  HRESULT beginDrawHR = m_sisNative->BeginDraw(
    updateRectNative, &surface, &offset);
  if (beginDrawHR == DXGI_ERROR_DEVICE_REMOVED ||
    beginDrawHR == DXGI_ERROR_DEVICE_RESET)
  {
    // If the device has been removed or reset, attempt to
    // re-create it and continue drawing.
    CreateDeviceResources();
    BeginDraw(updateRect);
  }
  // Create render target.
  ComPtr<ID2D1Bitmap1> bitmap;
  m_d2dContext->CreateBitmapFromDxgiSurface(
    surface.Get(),
    nullptr,
    &bitmap);
  // Set context's render target.
  m_d2dContext->SetTarget(bitmap.Get());
  // Begin drawing using D2D context.
  m_d2dContext->BeginDraw();
  // Apply a clip and transform to constrain updates to the target update
  // area. This is required to ensure coordinates within the target surface
  // remain consistent by taking into account the offset returned by
  // BeginDraw, and can also improve performance by optimizing the area
  // that's drawn by D2D. Apps should always account for the offset output
  // parameter returned by BeginDraw, because it might not match the passed
  // updateRect input parameter's location.
  m_d2dContext->PushAxisAlignedClip(
    D2D1::RectF(
      static_cast<float>(offset.x), 
      static_cast<float>(offset.y), 
      static_cast<float>(offset.x + updateRect.Width),
      static_cast<float>(offset.y + updateRect.Height)), 
      D2D1_ANTIALIAS_MODE_ALIASED);
    m_d2dContext->SetTransform(
      D2D1::Matrix3x2F::Translation(
        static_cast<float>(offset.x),
        static_cast<float>(offset.y)
        )
    );
}
// End drawing updates started by a previous BeginDraw call.
void MyImageSourceType::EndDraw()
{
  // Remove the transform and clip applied in BeginDraw because
  // the target area can change on every update.
  m_d2dContext->SetTransform(D2D1::IdentityMatrix());
  m_d2dContext->PopAxisAlignedClip();
  // Remove the render target and end drawing.
  m_d2dContext->EndDraw();
  m_d2dContext->SetTarget(nullptr);
  m_sisNative->EndDraw();
}

BeginDraw メソッドでは Rect プリミティブを入力として受け取り、ネイティブ RECT 型にマップしています。この RECT は対応する SurfaceImageSource を使用して描画する画面領域を定義します。ただし、BeginDraw は同時に 1 度しか呼び出せないため、SurfaceImageSource ごとの BeginDraw 呼び出しを順番にキューに追加する必要があります。

IDXGISurface への参照とオフセット POINT 構造体を初期化しているのがわかります。オフセット POINT 構造体は、IDXGISurface に描画する RECT の (x, y) オフセット座標を左上隅からの相対で指定します。このサーフェス ポインターとオフセットは、描画用の IDXGISurface を提供するために IDXGISurface ISurfaceImageSourceNative::BeginDraw から返されます。サンプルのその後の呼び出しでは、受け取ったサーフェス ポインターからビットマップを作成して Direct2D 呼び出しによりこれに描画します。EndDraw のオーバーロードで ISurfaceImageSourceNative::EndDraw を呼び出す時点で完了したイメージが、最終結果 (XAML image 要素またはプリミティブへの描画に利用できるイメージ) になります。

作成したのは以下のものです。

  • SurfaceImageSource から派生した型。
  • 画面上で指定した RECT への描画動作を定義する独自の派生型のメソッド。
  • 描画を実行する必要がある DirectX グラフィックス リソース。
  • DirectX グラフィックス デバイスと SurfaceImageSource の関連付け。

まだ必要なのは次のものです。

  • 実際のイメージを RECT にレンダリングするコード。
  • アプリから呼び出される、XAML 内の特定の <Image> インスタンス (またはプリミティブ) と SurfaceImageSource インスタンスとの接続。

描画動作のコードを作成するのは開発者の役割です。分離コードから呼び出される特定のパブリック メソッドとして SurfaceImageSource 型に実装するのがおそらく最もシンプルです。

残りは簡単です。XAML の分離コードで、以下のコードをコンストラクターに追加します。

// An image source derived from SurfaceImageSource,
// used to draw DirectX content.
MyImageSourceType^ _SISDXsource = ref new
  MyImageSourceType((int)MyDxImage->Width, (int)MyDxImage->Height, true);
// Use MyImageSourceType as a source for the Image control.
MyDxImage->Source = _SISDXsource;

同じ分離コードに次のようなイベント ハンドラーを追加します。

private void MainPage::MyCodeBehindObject_Click(
  Object^ sender, RoutedEventArgs^ e)
{
  // Begin updating the SurfaceImageSource.
  SISDXsource->BeginDraw();
  // ... Your DirectX drawing/animation calls here ...
  // such as _SISDXsource->
  // DrawTheMostAmazingSpinning3DShadedCubeEver();
  // ...
  // Stop updating the SurfaceImageSource and draw its contents.
  SISDXsource->EndDraw();
}

(代わりに、SurfaceImageSource から派生していない場合、前のコード スニペットの DrawTheMostAmazingSpinning3DShadedCubeEver などの描画オブジェクトのメソッドで BeginDraw と EndDraw への呼び出しを行うことができます)。

Rect、Ellipse などの XAML プリミティブを使用している場合は、次のように ImageBrush を作成して SurfaceImageSource をアタッチします (ここで MySISPrimitive は XAML グラフィックス プリミティブです)。

// Create a new image brush and set the ImageSource
// property to your SurfaceImageSource instance.
ImageBrush^ myBrush = new ImageBrush();
myBrush->ImageSource = _SISDXsource;
MySISPrimitive->Fill = myBrush;

これで作業は完了です。まとめると、今回の例のプロセスは次のようになります。

  1. レンダリングの対象とするイメージ、イメージ ブラシ、グラフィックス プリミティブ (Rect、Ellipse) などの XAML イメージ要素を選択します。また、サーフェスがアニメーション イメージを提供するかどうかも決めます。XAML に配置します。
  2. 描画操作に使用される DirectX デバイスおよびデバイス コンテキスト (特に Direct2D、Direct3D、または両方) を作成します。また、COM を使用して、SurfaceImageSource ランタイム型の基盤となる ISurfaceImageSourceNative インターフェイスを取得し、グラフィックス デバイスに関連付けます。
  3. ISurfaceImageSource::BeginDraw と ISurfaceImageSource::EndDraw を呼び出すコードを含む、SurfaceImageSource の派生型を作成します。
  4. 任意の特定の描画操作を SurfaceImageSource 型のメソッドとして追加します。
  5. Image サーフェスの場合は、Source プロパティを SurfaceImageSource 型のインスタンスに接続します。グラフィック プリミティブ サーフェスの場合は、ImageBrush を作成し、SurfaceImageSource インスタンスを ImageSource プロパティに割り当てます。その後、プリミティブの Fill プロパティ (ImageSource または ImageBrush を受け取る任意のプロパティ) で作成したブラシを使用します。
  6. イベント ハンドラーから SurfaceImageSource インスタンスの描画操作を呼び出します。アニメーション イメージの場合は、フレーム描画操作を必ず割り込み可能にします。

シーンやシェーダーがかなりシンプルなら、2D ゲームや 3D ゲームのシナリオに SurfaceImageSource を使用できます。たとえば、中級程度のグラフィックスを使用する戦略ゲーム ("シヴィライゼーション 4" など) やシンプルなダンジョン クローラーは、表示要素を SurfaceImageSource にレンダリングしています。

また、個別の DLL に SurfaceImageSource 型を C++ で作成し、C++ 以外のさまざまな言語プロジェクションからこの型を使用できます。この場合、レンダラーやメソッドを C++ に限定し、アプリのインフラスト ラクチャや分離コードを C# で構築できます。これはモデル - ビュー - ビューモデル (MVVM:Model-View-ViewModel) の規則です。

これには制限があります。

  • SurfaceImageSource を表示するコントロールは、固定サイズのサーフェス向けに設計されています。
  • SurfaceImageSource を表示するコントロールは、特にパンやズームを動的に実行する可能性がある任意の大きなサーフェス向けにはパフォーマンスが最適化されていません。
  • コントロールの更新は WinRT XAML フレームワーク ビュー プロバイダーによって処理されます。コントロールの更新は、フレームワークが更新されるときに行われます。リアルタイムで忠実度の高いグラフィックス シナリオの場合、これがパフォーマンスに著しく影響を与える可能性があります (つまり、シェーダーをかなり使用するホットで新しい銀河系バトル ゲームにはあまり適していません)。

このような制限事項があるため、VirtualSurfaceImageSource (最終的には SwapChainBackgroundPanel) を選ぶ動機になります。では、VirtualSurfaceImageSource から見てみましょう。

VirtualSurfaceImageSource と対話型コントロール レンダリング

VirtualSurfaceImageSource は SurfaceImageSource を拡張したものですが、ユーザーがサイズを変更する可能性のあるイメージ サーフェス向けに設計されています。特に、画面よりも大きいサイズに変更されるイメージや部分的に画面外に移動されるイメージ、他のイメージや XAML 要素で部分的または全体的に隠されるイメージなどが対象です。したがって、画面よりも大きくなる可能性があるイメージを一般にユーザーがパンまたはズームするアプリで特に効果を発揮します。

VirtualSurfaceImageSource のプロセスは先ほど紹介した SurfaceImageSource のプロセスと同様で、SurfaceImageSource の代わりに VirtualSurfaceImageSource 型、ISurfaceImageSourceNative インターフェイス実装の代わりに IVirtualImageSourceNative インターフェイス実装を使用するだけです。

つまり、先ほどの例のコードは次のように変更するだけです。

  • SurfaceImageSourc の代わりに VirtualSurfaceImageSource を使用します。次のコード サンプルでは、基本イメージ ソース型クラスの MyImageSourceType を VirtualSurfaceImageSource から派生します。
  • 基盤となる IVirtualSurfaceImageSourceNative インターフェイスのメソッド実装にクエリを実行します。

図 4 の例をご覧ください。

図 4 VirtualSurfaceImageSource から継承

public ref class MyImageSourceType sealed : Windows::UI::Xaml::Media::Imaging::VirtualSurfaceImageSource
{
  // ...
  MyImageSourceType::MyImageSourceType(
    int pixelWidth,
    int pixelHeight,
    bool isOpaque
    ) : VirtualSurfaceImageSource(pixelWidth, pixelHeight, isOpaque)
  {
    // Global variable that contains the width, in pixels,
    // of the SurfaceImageSource.
    m_width = pixelWidth;
    // Global variable that contains the height, in pixels,
    // of the SurfaceImageSource.
    m_height = pixelHeight;
    CreateDeviceIndependentResources(); // See below.
    CreateDeviceResources(); //Set up the DXGI resources.
  }
  // ...
  void MyImageSourceType::CreateDeviceIndependentResources()
  {
    // Query for IVirtualSurfaceImageSourceNative interface.
    reinterpret_cast<IUnknown*>(this)->QueryInterface(
      IID_PPV_ARGS(&m_vsisNative));
  }
  // ...
}

非常に重要な違いがもう 1 つあります。サーフェスの "タイル" (定義済みの矩形領域のことで、Windows 8 の UI タイルではありません) が可視になり、描画が必要になるときに必ず呼び出されるコールバックを実装しなけれなりません。このようなタイルは、アプリが VirtualSurfaceImageSource インスタンスを作成するときにフレームワークによって管理されるため、開発者がタイルのパラメーターを管理することはありません。また、内部では大きなイメージがタイルに分割されているため、タイルの一部がユーザーに表示され更新が必要になると常にコールバックが呼び出されます。

このメカニズムを使用するには、まず、IVirtualSurfaceUpdatesCallbackNative インターフェイスから継承するインスタンス化可能な型を実装し、IVirtualSurfaceImageSource::RegisterForUpdatesNeeded にこの型のインスタンスを渡して登録する必要があります (図 5 参照)。

図 5 VirtualSurfaceImageSource のコールバックの設定

class MyVisibleSurfaceDrawingType :
  public IVirtualSurfaceUpdatesCallbackNative
{
// ...
private:
  virtual HRESULT STDMETHODCALLTYPE UpdatesNeeded() override;
}
// ...
HRESULT STDMETHODCALLTYPE MyVisibleSurfaceDrawingType::UpdatesNeeded()
{
  // ... perform drawing here ...
}
void MyVisibleSurfaceDrawingType::Initialize()
{
  // ...
  m_vsisNative->RegisterForUpdatesNeeded(this);
  // ...
}

描画操作は、IVirtualSurfaceUpdatesCallbackNative インターフェイスの UpdatesNeeded メソッドとして実装します。特定の領域が可視になる場合、どのタイルを更新するかを決める必要があります。そのためには、IVirtualSurfaceImageSourceNative::GetRectCount を呼び出し、更新するタイル数が 1 以上であれば、IVirtualSurfaceImageSourceNative::GetUpdateRects を使用して更新するタイルの特定の矩形を取得して各タイルを更新します。

HRESULT STDMETHODCALLTYPE MyVisibleSurfaceDrawingType::UpdatesNeeded()
{
  HRESULT hr = S_OK;
  ULONG drawingBoundsCount = 0; 
  m_vsisNative->GetUpdateRectCount(&drawingBoundsCount);
  std::unique_ptr<RECT[]> drawingBounds(new RECT[drawingBoundsCount]);
  m_vsisNative->GetUpdateRects(drawingBounds.get(), drawingBoundsCount);
  for (ULONG i = 0; i < drawingBoundsCount; ++i)
  {
    // ... per-tile drawing code here ...
  }
}

これらのタイルの VirtualSurfaceImageSource 定義のパラメーターを RECT オブジェクトとして取得できます。前の例では、更新が必要なすべてのタイルのために RECT オブジェクトの配列を取得しています。その後、このRECT の値を渡して VirtualSurfaceImageSource::BeginDraw を呼び出すことでタイルを再描画します。

ここでも SurfaceImageSource と同様に、IDXGISurface へのポインターを初期化し、IVirtualSurfaceImageSourceNative (基盤となるネイティブ インターフェイス実装) の BeginDraw メソッドを呼び出します。ただし、オフセットはイメージ要素全体のオフセットではなく、ターゲットとなる RECT の (x, y) オフセットになります。

更新する RECT ごとに、図 6 のようなコードを呼び出します。

図 6 コントロール サイズまたは表示に対する更新の処理

POINT offset;
ComPtr<IDXGISurface> dynamicSurface;
// Set offset.
// Call the following code once for each tile RECT that
// needs to be updated.
HRESULT beginDrawHR = m_vsisNative->
  BeginDraw(updateRect, &dynamicSurface, &offset);
if (beginDrawHR == DXGI_ERROR_DEVICE_REMOVED ||
  beginDrawHR == DXGI_ERROR_DEVICE_RESET)
{
  // Handle the change in the graphics interface.
}
else
{
  // Draw to IDXGISurface for the updated RECT at the provided offset.
}

繰り返しになりますが、グラフィックス インターフェイスは UI スレッドで同時に 1 つの操作しか実行できないため、これらの呼び出しを並列処理することはできません。各タイルの RECT を順番に処理することも、1 回で描画を更新するためにすべての RECT を 1 つの領域にまとめて IVirtualSurfaceImageSourceNative::BeginDraw を呼び出すこともできます。どのように処理するかは開発者に委ねられています。

最後に、変化した各タイルの RECT を更新後 IVirtualSurfaceImageSourceNative::EndDraw を呼び出します。最後に更新するタイルを処理すると、SurfaceImageSource の例と同様、対応する XAML イメージやプリミティブに提供するための完全に更新されたビットマップが用意されます。

これで作業は完了です。この形式の DirectX-XAML 相互運用は、精緻でリアルタイムのゲームのように、リアルタイム 3D グラフィックスの待ち時間が短い入力を気にかける必要はありません。また、豊富なグラフィックスを使用するアプリやコントロール、非同期 (読み取り: ターン制の) ゲームにも適しています。

次回は、もう 1 つのアプローチについて説明します。DirectX スワップ チェーンの上位に XAML を描画するこのアプローチは、XAML フレームワークがカスタム DirectX ビュー プロバイダーと上手く連携することを必要とします。お見逃しなく。

Get help building your Azure app!

Doug Erickson は、Windows コンテンツ サービスに取り組み、DirectX と Windows ストア ゲーム開発を専門とするマイクロソフトのシニア プログラミング ライターです。彼は、読者が驚くべき Windows ストア DirectX ゲームを多く作成して有名になることを望んでいます。

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