本文章是由機器翻譯。

DirectX

在 Windows 應用商店的應用中將 XAML 與 DirectX 和 C++ 結合使用

Doug Erickson

 

自 Windows Vista 以來,DirectX 成為了 Windows 平臺的核心圖形 API,使所有作業系統螢幕繪製操作都實現了圖形處理單元 (GPU) 加速。 不過,在 Windows 8 之前,DirectX 開發人員必須使用本機 C++ 和 COM 從頭構建自己的 UI 框架,或者獲許使用中介軟體 UI 套裝程式,如 Scaleform。

在 Windows 8,您可以利用 Windows 運行時 (WinRT) 的 DirectX-XAML 交互操作功能,彌補本機 DirectX 與適用 UI 框架之間的差異。 要利用 DirectX 中針對 XAML 的 API 支援,您需要使用「本機」C++(儘管您可以訪問智慧指標和 C++ 元件擴展)。 雖然具備一些 COM 基本知識也會有所説明,不過我會具體指出在將 XAML 框架與 DirectX 操作結合時都用到了哪些交互操作。

本系列文章分為兩個部分,分別介紹兩種實現 DirectX-XAML 之間交互操作的方法:一種方法是使用 DirectX 圖形 API 將圖面繪製到 XAML 框架元素之內;另一種方法是在 DirectX 交換鏈圖面之上繪製 XAML 控制項層次結構。

本文將介紹第一種方案,即將內容呈現給 XAML 框架中顯示的圖像或基元。

開始之前,我們首先簡要概述一下 API 選項。 現在,Windows 運行時中提供了三種支援 DirectX 交互操作的 XAML 類型:

  • Windows::UI::Xaml::Media::Imaging::SurfaceImage­Source(下文簡稱 SurfaceImageSource):這種類型可用於使用 DirectX 圖形 API 將相對靜態的內容繪製到 XAML 共用圖面。 視圖完全由 WinRT XAML 框架管理,這意味著所有呈現元素也同樣由該框架管理。 這十分適合繪製畫面複雜但不會逐幀變化的內容,但對於更新頻繁的複雜 2D 或 3D 遊戲,繪製效果並不理想。
  • Windows::UI::Xaml::Media::Imaging::VirtualSurface­ImageSource(下文簡稱 VirtualSurfaceImageSource):與 SurfaceImageSource 相同的是,這種類型使用為 XAML 框架定義的圖形資源。 與 SurfaceImage­Source 不同的是,VirtualSurfaceImageSource 支援邏輯劃分的大圖面,並進行了分區域優化,以便 DirectX 僅繪製在兩次更新之間發生變化的圖面區域。 比如,如果您要創建地圖控制項或圖像密集的大型文檔檢視器,可選擇此元素。 同樣,與 SurfaceImageSource 一樣,此元素也不太適合複雜的 2D 或 3D 遊戲,尤其是依賴于即時畫面和回饋的遊戲。
  • Windows::UI::Xaml::Controls::SwapChainBackgroundPanel(下文簡稱 SwapChainBackgroundPanel):這種類型的 XAML 控制項元素使應用程式可以使用自訂 DirectX 視圖提供程式(和交換鏈),您可在上面繪製 XAML 元素,並且可在需要很低延遲呈現或高頻率回饋的情況下(例如,現代遊戲)實現較好的性能。 應用程式將獨立于 XAML 框架為 SwapChainBackgroundPanel 管理 DirectX 設備上下文。 當然,這意味著 SwapChainBackgroundPanel 和 XAML 框架在刷新時並不相互同步。 您也可以通過後台執行緒將內容呈現給 SwapChainBackgroundPanel。

現在,我將介紹 SurfaceImageSource 和 Virtual­SurfaceImageSource API,以及如何將它們合併到圖像和媒體富集的 XAML 控制項(SwapChainBackgroundPanel 十分特殊,將在其他文章中單獨予以介紹)。

Note:SurfaceImageSource 和 VirtualSurfaceImageSource 可在 C# 或 Visual Basic .NET 中使用,但 DirectX 呈現元件必須用 C++ 編寫並編譯為一個獨立的 DLL,以便 C# 專案使用。 此外,還有協力廠商管理的 WinRT DirectX 框架,如 SharpDX (sharpdx.org) 和 MonoGame (monogame. net), which you can use instead of SurfaceImageSource or VirtualSurfaceImageSource.

好了,讓我們步入正題。 本文假定您瞭解 DirectX 的基本知識,特別是 Direct2D、Direct3D 和 Microsoft DirectX 圖形基礎結構 (DXGI)。 當然,您應熟悉 XAML 和 C++;這是中級 Windows 應用程式開發人員應有的技能。 請系好安全帶, onward!

SurfaceImageSource 和 DirectX 圖像複合

Windows::UI::Xaml::Media::Imaging 命名空間包含 SurfaceImageSource 類型以及諸多其他 XAML 影像處理類型。 實際上,SurfaceImageSource 類型提供了一種方法,可動態繪製到許多 XAML 圖形和圖像基元的共用圖面,從而有效地將 DirectX 圖形調用所呈現的內容填充到這些圖面,並且將其應用為畫筆。 (具體來說,它是用作 ImageBrush 的 ImageSource。)可以將其想像成一個用 DirectX 快速生成的點陣圖,並且將該類型用於可應用點陣圖或其他圖像資源的許多地方。

為了配合本節的目標,我將內容繪製到 <Image> XAML 元素,其中包含一個作為預留位置的空白 PNG 圖像。 我為 <Image> 元素指定了高度和寬度,因為此資訊將通過代碼傳遞給 SurfaceImageSource 建構函式(如果未提供高度和寬度,那麼我呈現的內容將會拉伸以適合 <Image> 標記參數):

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

在此例中,我的目標是 <Image> 標記,它將顯示用於承載繪製內容的圖面。 我也可以使用 XAML 基元,比如 <Rectangle> 或 <Ellipse>,它們均可由 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 運行時庫 (WRL) 的標頭檔,一些關鍵的 DirectX 元件以及最重要的本機 DirectX 交互操作介面。 加入最後一條語句的必要性將很快會變得顯而易見。

我還導入了相應的庫: dxgi.lib, d2d1.lib and 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();
  }
  // ...
}

Note:儘管並不需要從 SurfaceImageSource 繼承,但從代碼組織角度來說這樣做會更容易些。 您只需將 SurfaceImageSource 物件具現化為成員,然後使用該成員即可。 但要記住,代碼示例中的成員名代表的是物件的自引用 (this)。

CreateDeviceResources 和 CreateDeviceIndependent­Resources 方法由使用者來實現,通過這種方式可以方便地將特定于 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 方法中,我中斷 SurfaceImageSource 上所定義的本機方法的 COM 及查詢。 由於這些方法不是直接公開的,因此必須通過在 SurfaceImageSource 或 SurfaceImageSource 派生的類型上調用 IUnknown::Query­Interface 來獲取。 為此,我將 SurfaceImageSource 派生的類型重新強制轉換為 IUnknown,這是任何 COM 介面的基介面(我也可將其強制轉換為 IInspectable,這是從 IUnknown 繼承的任何 WinRT 類型的「基」介面)。 然後,為得到 ISurfaceImageSourceNative 方法的清單,我對該介面執行了查詢,如下所示:

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

(IID_PPV_ARGS 是檢索介面指標的 WRL 的輔助宏。 Very convenient! 如果您沒有從 SurfaceImageSource 繼承,請用 SurfaceImageSource 物件的成員名稱替代。)

最後,CreateDeviceResources 方法的以下這一部分也就十分清楚易懂:

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

ISurfaceImageSourceNative::SetDevice 獲取配置好的圖形介面並將其與圖面掛接在一起,用以執行任意繪製操作。 當然,請注意,這也意味著應該在至少先調用一次 CreateDeviceIndependentResources 之後再調用 Create­DeviceResources,否則我們無法得到一個配置好的設備用於掛接。

現在,我已公開了派生 MyImageSourceType 類型的 SurfaceImageSource 類型的底層 ISurfaceImageSourceNative 實現。 我實際上打開了封裝並將綁定轉移給 SurfaceImageSource 類型,而不是移到繪製調用的基實現,因此這並不是我自己的。 現在,我將實現這些調用。

為此,我將實現下列方法:

  • BeginDraw:此方法打開用於繪製的設備上下文。
  • EndDraw:此方法關閉設備上下文。

Note:我選擇了名為 BeginDraw 和 EndDraw 的方法,與 ISurfaceImageSourceNative 方法之間有些鬆散的對應關係。 提供此模式是為了簡單起見,不是強制性的。

在某些情況下,我的 BeginDraw 方法(或在派生類型上定義的其他繪製初始化方法)必須調用 ISurface­ImageSourceNative::BeginDraw。 (為了優化,可以為要更新的圖像區域的子矩形添加一個參數。)同樣,EndDraw 方法也應調用 ISurface­ImageSourceNative::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 一次只能調用一個;我將不得不依次對每個 SurfaceImageSource 的 BeginDraw 調用進行排隊。

另請注意,我初始化了一個對 IDXGISurface 的引用以及一個偏移 POINT 結構,後者包含將繪製到 IDXGISurface 的 RECT 的 (x, y) 偏移座標(相對於左上角)。 此圖面指標和偏移將從 ISurfaceImageSourceNative::BeginDraw 返回,以提供用於繪製的 IDXGISurface。 此示例中以後的調用將從接收到的圖面指標創建點陣圖,然後通過 Direct2D 調用將其繪製到圖面。 在 EndDraw 重載中調用 ISurfaceImageSourceNative::EndDraw 時,所完成的圖像就是最終結果,也就是可用於繪製到 XAML 圖像元素或基元的圖像。

讓我們來看一下我所得到的結果:

  • 從 SurfaceImageSource 派生的類型。
  • 派生類型上定義了繪製到螢幕上提供的 RECT 的行為的方法。
  • 需要執行繪製的 DirectX 圖形資源。
  • DirectX 圖形設備與 SurfaceImageSource 之間的關聯。

What I still need is:

  • 一些將實際圖像呈現到 RECT 的代碼。
  • XAML 中的特定 <Image> 實例(或基元)與 SurfaceImageSource 實例(將由應用程式調用)之間的連接。

我可以自行決定繪製行為的代碼,可能最簡單的操作就是在 SurfaceImageSource 類型上將該行為實現為可從代碼隱藏檔調用的特定公共方法。

The rest is easy. 在 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 派生,我可將對 BeginDraw 和 EndDraw 的調用放置在我的繪製物件的一個方法中(如以前程式碼片段中的 DrawTheMostAmazingSpinning3DShadedCubeEver))。

現在,我使用的是 XAML 基元(如 Rect 或 Ellipse),我將創建一個 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. 選擇要呈現到的 XAML 圖像元素(如 Image、ImageBrush)或圖形基元(Rect、Ellipse 等)。 此外,確定圖面是否將提供動畫圖像。 將圖面放在 XAML 中。
  2. 創建將用於執行繪製操作的 DirectX 設備和設備上下文(通常為 Direct2D 和/或 Direct3D)。 此外,使用 COM 獲取對 ISurfaceImageSourceNative 介面的引用,該介面支援 SurfaceImageSource 運行時類型並將圖形設備與之關聯。
  3. 創建一個從 SurfaceImageSource 派生的類型,該類型包含調用 ISurfaceImageSource::BeginDraw 和 ISurfaceImageSource::EndDraw 的代碼。
  4. 將任何特定的繪製操作作為方法添加到 SurfaceImageSource 類型。
  5. 對於 Image 圖面,將 Source 屬性連接到 SurfaceImageSource 類型實例。 對於圖形基元圖面,創建 ImageBrush 並為 ImageSource 屬性指定一個 SurfaceImage­Source 實例,然後將以 Fill 屬性(或接受 ImageSource 或 ImageBrush 的任何屬性)使用該畫筆。
  6. 通過事件處理常式在 SurfaceImageSource 實例上調用繪製操作。 對於動畫圖像,確保幀繪製操作可中斷。

如果場景和著色相當簡單,可對 2D 和 3D 遊戲方案使用 Surface­ImageSource。 例如,圖形複雜程度中等的兵法遊戲(如「Civilization 4」)或簡單的地牢探索者遊戲可將畫面呈現到 SurfaceImageSource。

另請注意,我可用 C++ 創建 SurfaceImageSource 派生類型並將其封裝在一個單獨的 DLL 中,然後從非 C++ 語言投射使用該類型。 在此例中,我可將呈現器和方法限定為 C++,而通過 C# 模型-視圖-視圖模型 (MVVM) 規則構建應用程式基礎結構和 代碼隱藏檔!

這會給我們帶來下列限制:

  • 顯示 SurfaceImageSource 的控制項針對固定大小的圖面而設計。
  • 顯示 SurfaceImageSource 的控制項不是針對任意大圖面進行性能優化的,尤其是可動態平移或縮放的圖面。
  • 控制項刷新由 WinRT XAML 框架視圖提供程式處理,在框架刷新時執行該提供程式。 對於即時高保真圖形,這可能會顯著影響性能(意味著並不十分適合最新的熱門著色密集型星球大戰遊戲)。

這使我們將使用 VirtualSurfaceImageSource(最終變為 SwapChainBackgroundPanel)。 讓我們看一下前者。

VirtualSurfaceImageSource 和互動式控制項呈現

VirtualSurfaceImageSource 是 SurfaceImageSource 的擴展,但適用于可由使用者調整大小的圖像圖面,尤其是大於螢幕或部分移離螢幕的圖像或者可能包含部分或全部遮蔽的其他圖像或 XAML 元素的圖像。 它特別適用于使用者經常平移或縮放可能大於螢幕的圖像的應用程式,例如,圖像檢視器的地圖控制項。

VirtualSurfaceImageSource 的執行過程與前面所述的 SurfaceImageSource 相同,您只需用 VirtualSurfaceImageSource 類型替代 SurfaceImageSource,並用 IVirtualImageSourceNative 介面實現替代 ISurfaceImageSourceNative 介面實現即可。

這意味著我應對上述示例中的代碼進行如下更改:

  • Use VirtualSurfaceImageSource instead of SurfaceImage­Source. 在以下代碼示例中,我將具有從 VirtualSurfaceImageSource 派生的基圖像源類型類 MyImageSourceType。
  • 查詢底層 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));
  }
  // ...
}

哦,還存在一個非常重要的差異:我必須實現一個回檔,每次圖面的「磁貼」(一個定義的矩形區域,不要將其與 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 方法。 如果特定區域變為可見,我必須確定應更新哪些磁貼。 為此,我將調用 IVirtualSurfaceImage­SourceNative::GetRectCount;如果已更新的磁貼計數大於零,則使用 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 執行緒執行一項操作。 我可以連續處理每個磁貼 RECT,也可以對所有 RECT 的合併區域調用 IVirtualSurface­ImageSourceNative::BeginDraw 以執行一次性繪製更新。 這將取決於開發人員。

最後,我在更新每個更改的磁貼 RECT 後,調用 IVirtualSurfaceImageSourceNative::EndDraw。 在處理最後一個更新的磁貼後,我將得到一個完全更新的點陣圖以提供給相應的 XAML 圖像或基元,就像在 SurfaceImageSource 示例中執行的操作一樣。

就是這樣! 如果使用者並不在乎即時 3D 圖形的低延遲輸入,因為他們可能在玩十分精細的即時遊戲,那麼這種形式的 DirectX-XAML 交互操作很不錯。 此外,對於圖形豐富的應用程式和控制項以及比較非同步(意思是輪流參與)的遊戲也非常棒。

在後續文章中,我將介紹以下這一方法的利弊:基於 DirectX 交換鏈繪製 XAML;此外還將介紹使用自訂 DirectX 視圖提供程式實現 XAML 框架流暢播放所需的工作。 請繼續關注!

Doug Erickson is 是 Microsoft 的高級程式師,他供職于 Windows Content Services 部門,專門從事 DirectX 和 Windows 應用商店遊戲開發工作。他希望您能開發許多令人震撼的 Windows Store DirectX 遊戲並一舉成名。

衷心感謝以下技術專家對本文的審閱: Jesse Bishop and Bede Jordan