此文章由机器翻译。

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 十分特殊,将在其他文章中单独予以介绍)。

注: SurfaceImageSource 和 VirtualSurfaceImageSource 可在 C# 或 Visual Basic .NET 中使用,但 DirectX 呈现组件必须用 C++ 编写并编译为一个独立的 DLL,以便 C# 项目使用。此外,还有第三方管理的 WinRT DirectX 框架,如 SharpDX (sharpdx.org) 和 MonoGame (monogame.net),您可用其替代 SurfaceImageSource 或 VirtualSurfaceImageSource。

好了,让我们步入正题。本文假定您了解 DirectX 的基本知识,特别是 Direct2D、Direct3D 和 Microsoft DirectX 图形基础结构 (DXGI)。当然,您应熟悉 XAML 和 C++;这是中级 Windows 应用程序开发人员应有的技能。请系好安全带, 走你!

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 和 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 和 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 的辅助宏。 非常方便! 如果您没有从 SurfaceImageSource 继承,请用 SurfaceImageSource 对象的成员名称替代。)

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

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

ISurfaceImageSourceNative::SetDevice 获取配置好的图形接口并将其与图面挂接在一起,用以执行任意绘制操作。 当然,请注意,这也意味着应该在至少先调用一次 CreateDeviceIndependentResources 之后再调用 Create­DeviceResources,否则我们无法得到一个配置好的设备用于挂接。

现在,我已公开了派生 MyImageSourceType 类型的 SurfaceImageSource 类型的底层 ISurfaceImageSourceNative 实现。 我实际上打开了封装并将绑定转移给 SurfaceImageSource 类型,而不是移到绘制调用的基实现,因此这并不是我自己的。 现在,我将实现这些调用。

为此,我将实现下列方法:

  • BeginDraw: 此方法打开用于绘制的设备上下文。
  • EndDraw: 此方法关闭设备上下文。

注: 我选择了名为 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 之间的关联。

我仍需要完成的工作是:

  • 一些将实际图像呈现到 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 派生,我可将对 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 接口实现即可。

这意味着我应对上述示例中的代码进行如下更改:

  • 使用 VirtualSurfaceImageSource 替代 SurfaceImageSource。 在以下代码示例中,我将具有从 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 框架流畅播放所需的工作。请继续关注!

Get help building your Windows Store app!

Doug Erickson is 是 Microsoft 的高级程序员,他供职于 Windows Content Services 部门,专门从事 DirectX 和 Windows 应用商店游戏开发工作。 他希望您能开发许多令人震撼的 Windows Store DirectX 游戏并一举成名。

衷心感谢以下技术专家对本文的审阅: Jesse Bishop 和 Bede Jordan