DirectX

Utilisation de XAML avec DirectX et C++ dans les applications du Windows Store

Doug Erickson

 

Depuis Windows Vista, DirectX constitue l'API graphique principale de la plateforme Windows, ce qui permet une accélération de l'unité de traitement graphique (GPU) pour toutes les opérations de dessin à l'écran du système d'exploitation. Toutefois, jusqu'à Windows 8, les développeurs DirectX devaient déployer leurs propres infrastructures d'interface utilisateur de bout en bout en C++ natif et COM, ou se procurer sous licence un package d'interface utilisateur intermédiaire, tel que Scaleform.

Dans Windows 8, vous pouvez combler le fossé existant entre DirectX natif et une infrastructure d'interface utilisateur adéquate avec la fonctionnalité d'interopérabilité DirectX-XAML de Windows Runtime (WinRT). Pour tirer parti de la prise en charge de l'API pour XAML dans DirectX, vous devez utiliser C++ « natif » (même si vous avez accès à des pointeurs intelligents et aux extensions de composants C++). Quelques connaissances de base de COM sont également utiles, même si je vais décrire clairement l'interopérabilité spécifique à implémenter pour associer l'infrastructure XAML et les opérations DirectX.

Dans cette série d'articles en deux parties, je vais examiner deux approches de l'interopérabilité DirectX-XAML : l'une dans laquelle je dessine des surfaces dans des éléments d'infrastructure XAML avec les API graphiques DirectX et l'autre dans laquelle je dessine une hiérarchie de contrôles XAML sur une surface de chaîne de permutation DirectX.

Cet article porte sur le premier scénario, dans lequel vous restituez des images ou primitives affichées dans votre infrastructure XAML.

Mais, tout d'abord, voici un aperçu rapide de vos options d'API. Pour le moment, dans Windows Runtime, trois types XAML prennent en charge l'interopérabilité DirectX :

  • Windows::UI::Xaml::Media::Imaging::SurfaceImage­Source (ci-après SurfaceImageSource) : ce type vous permet de dessiner un contenu relativement statique sur une surface XAML partagée à l'aide des API graphiques DirectX. L'affichage est entièrement géré par l'infrastructure XAML WinRT, ce qui signifie que tous les éléments de présentation sont également gérés par celle-ci. Ce type est par conséquent idéal pour dessiner un contenu complexe qui ne modifie pas chaque trame, mais convient moins pour les jeux en 2D et 3D complexes qui sont très fréquemment mis à jour.
  • Windows::UI::Xaml::Media::Imaging::VirtualSurface­ImageSource (ci-après VirtualSurfaceImageSource) : Comme SurfaceImageSource, ce type utilise les ressources graphiques définies pour l'infrastructure XAML. À la différence de SurfaceImage­Source, VirtualSurfaceImageSource prend en charge des surfaces logiquement importantes de façon optimisée et selon la région, de sorte que DirectX dessine uniquement les régions de la surface modifiées entre les mises à jour. Choisissez cet élément si vous créez, par exemple, un contrôle de carte ou une visionneuse de documents avec beaucoup d'images et de grande taille. Là encore, comme SurfaceImageSource, ce type ne représente pas un bon choix pour les jeux en 2D et 3D complexes, en particulier ceux qui reposent sur des réactions et des effets visuels en temps réel.
  • Windows::UI::Xaml::Controls::SwapChainBackgroundPanel (ci-après SwapChainBackgroundPanel) : Ce type et élément de contrôle XAML permet à votre application d'utiliser un fournisseur d'affichage DirectX personnalisé (et une chaîne de permutation) sur lequel vous pouvez dessiner des éléments XAML et qui permet d'améliorer les performances dans des scénarios qui exigent une présentation à très faible latence ou des réactions à haute fréquence (les jeux modernes, par exemple). Votre application gère le contexte de périphérique DirectX pour SwapChainBackgroundPanel séparément de l'infrastructure XAML. Bien entendu, cela signifie que les trames SwapChainBackgroundPanel et XAML ne sont pas synchronisées entre elles pour l'actualisation. Vous pouvez également restituer une trame SwapChainBackgroundPanel à partir d'un thread en arrière-plan.

À présent, je vais examiner les API SurfaceImageSource et Virtual­SurfaceImageSource, et la façon de les incorporer à vos contrôles XAML d'images et multimédias enrichis (SwapChainBackgroundPanel est particulier et fait l'objet d'un article à part).

Remarque : SurfaceImageSource et VirtualSurfaceImageSource peuvent être utilisés à partir de C# ou Visual Basic .NET, même si le composant de rendu DirectX doit être écrit en C++ et compilé dans une DLL séparée accessible à partir du projet C#. Il existe également des infrastructures DirectX WinRT gérées tierces, telles que SharpDX (sharpdx.org) et MonoGame (monogame.net), que vous pouvez utiliser à la place de SurfaceImageSource ou VirtualSurfaceImageSource.

Commençons donc. Cet article part du principe que vous avez acquis les principes de base de DirectX, en particulier Direct2D, Direct3D et l'infrastructure Microsoft DirectX Graphics Infrastructure (DXGI). Vous connaissez bien évidemment XAML et C++ ; il s'agit d'un sujet de niveau moyen pour les développeurs d'applications Windows. Si vous êtes prêt : en avant !

SurfaceImageSource et composition d'image DirectX

L'espace de noms Windows::UI::Xaml::Media::Imaging contient le type SurfaceImageSource ainsi qu'un grand nombre des autres types de création d'images XAML. En fait, le type SurfaceImageSource permet de dessiner de façon dynamique sur les surfaces partagées d'un grand nombre de primitives de création d'images et de graphiques XAML, en les remplissant avec le contenu restitué avec les appels de graphiques DirectX et en les appliquant sous la forme d'un pinceau. (Concrètement, il s'agit d'un élément ImageSource que vous utilisez comme un élément ImageBrush.) Considérez-le comme une image bitmap que vous générez à la volée avec DirectX et envisagez de l'utiliser à de nombreux emplacements où vous pourriez appliquer une image bitmap ou une autre ressource d'image.

Dans le cadre de cette section, je vais dessiner dans un élément XAML <Image> qui contient une image PNG vide en tant qu'espace réservé. J'indique une hauteur et une largeur pour l'élément <Image>, car ces informations sont transmises au constructeur SurfaceImageSource de mon code (si je n'indique pas ces valeurs, le contenu restitué sera étiré pour s'adapter aux paramètres de la balise <Image>) :

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

Dans cet exemple, ma cible est la balise <Image>, qui affiche la surface sur laquelle je dessine. Je pourrais aussi bien utiliser une primitive XAML, telle que <Rectangle> ou <Ellipse>, les deux pouvant être remplies par un pinceau SurfaceImageSource. Cela est possible, car les dessins de ces primitives et images sont effectués avec DirectX par Windows Runtime ; je ne fais qu'y associer une source de rendu différente en arrière-plan, en quelque sorte.

Dans mon code, j'inclus les éléments suivants :

#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"

Il s'agit des en-têtes de la bibliothèque Windows Runtime (WRL), de quelques composants DirectX clés et, surtout, des interfaces d'interopérabilité DirectX natives. Le besoin d'inclure ces dernières deviendra bientôt évident.

J'importe également les bibliothèques correspondantes : dxgi.lib, d2d1.lib et d3d11.lib.

Par ailleurs, pour des raisons pratiques, je vais également inclure les espaces de noms suivants :

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

Maintenant, dans le code, je crée un type, MyImageSourceType, qui hérite du type SurfaceImageSource de base et appelle son constructeur, comme illustré à la figure 1.

Figure 1 Dérivation de 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();
  }
  // ...
}

Remarque : il n'est pas nécessaire d'hériter de SurfaceImageSource, même si cela rend les choses un peu plus faciles du point de vue de l'organisation du code. Vous pouvez simplement instancier un objet SurfaceImageSource en tant que membre et l'utiliser à la place. Il vous suffit de remplacer mentalement l'auto-référence de l'objet (this) par le nom de votre membre dans les exemples de code.

Les méthodes CreateDeviceResources et CreateDeviceIndependent­Resources sont des implémentations utilisateur qui représentent un moyen pratique de séparer logiquement la configuration spécifique à l'interface matérielle graphique DirectX de la configuration spécifique aux applications DirectX plus générales. Les actions entreprises dans les deux méthodes sont essentielles. Toutefois, il est judicieux (et nécessaire) lors de la conception de les séparer, car il peut arriver que vous vouliez recréer les ressources de périphérique sans affecter les ressources indépendantes du périphérique, et inversement.

CreateDeviceResources doit ressembler au code dans la figure 2, au moins sous sa forme de base.

Figure 2 Création des ressources spécifiques au périphérique 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());
}

À ce stade, j'ai créé un contexte de périphérique matériel et je l'ai associé à … attendez, qu'est ce qu'ISurfaceImageSourceNative ? Il ne s'agit pas d'un type WinRT ! Que se passe-t-il donc ?

Il s'agit du bit d'interopérabilité. C'est là où je me faufile dans les « tubes de Jefferies » de la bibliothèque Windows Runtime (WRL) et procède à quelque réfection. C'est également là où j'accède à l'interface COM qui se trouve derrière une grande partie de la bibliothèque WRL.

Afin d'activer ce comportement d'interopérabilité, je dois essentiellement brancher cette source DirectX sous le capot. Pour ce faire, je dois intégrer mon type à l'implémentation des méthodes définies dans l'interface COM spécifique à la bibliothèque WRL, ISurfaceImageSourceNative. Une fois cette opération effectuée, j'attache le type à l'élément <Image> (dans cet exemple) et, lorsque l'application impose une mise à jour à l'infrastructure XAML, elle utilise mes implémentations DirectX des appels de dessin au lieu de celles par défaut.

L'interface ISurfaceImageSourceNative est définie dans l'en-tête d'interopérabilité que j'ai mentionné plus tôt. Vous voyez ce qui se passe ici ?

Maintenant, dans ma méthode CreateDeviceIndependentResources spécifique à l'application, je quitte l'interface COM et recherche les méthodes natives définies sur SurfaceImageSource. Comme ces méthodes ne sont pas exposées directement, elles doivent être obtenues avec un appel à IUnknown::Query­Interface sur SurfaceImageSource ou le type dérivé de SurfaceImageSource. Pour ce faire, je remanie mon type dérivé de SurfaceImageSource en IUnknown, l'interface de base de toute interface COM (je pourrais également le convertir en IInspectable, l'interface de base de tout type WinRT, qui hérite d'IUnknown). Ensuite, pour obtenir une liste des méthodes ISurfaceImageSourceNative, j'interroge cette interface, comme ceci :

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

(IID_PPV_ARGS est une macro d'assistance pour la bibliothèque WRL qui récupère un pointeur d'interface. Très pratique ! Si vous n'héritez pas de SurfaceImageSource, remplacez this par le nom de membre de l'objet SurfaceImageSource.)

Cette partie de la méthode CreateDeviceResources a enfin un sens :

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

ISurfaceImageSourceNative::SetDevice prend l'interface graphique configurée et l'associe à la surface pour toutes les opérations de dessin. Notez cependant que cela signifie également que je dois appeler Create­DeviceResources après avoir appelé CreateDeviceIndependentResources au moins une fois au préalable ou je n'aurai pas de périphérique configuré à attacher.

J'ai à présent exposé l'implémentation ISurfaceImageSourceNative sous-jacente du type SurfaceImageSource à partir duquel dérive mon type MyImageSourceType. J'ai réellement ouvert le capot et déplacé les câbles vers le type SurfaceImageSource, même s'il s'agit de l'implémentation de base des appels de dessin et non de la mienne. Je vais maintenant implémenter mes appels.

Pour ce faire, j'implémente les méthodes suivantes :

  • BeginDraw : cette méthode ouvre le contexte de périphérique pour le dessin.
  • EndDraw : cette méthode ferme le contexte de périphérique.

Remarque : j'ai choisi les noms de méthode BeginDraw et EndDraw pour une sorte de vague correspondance avec les méthodes ISurfaceImageSourceNative. Ce modèle est utilisé pour des raisons pratiques et n'est pas appliqué.

Ma méthode BeginDraw (ou toute autre méthode d'initialisation de dessin que je définis sur le type dérivé) doit, à un moment donné, appeler ISurface­ImageSourceNative::BeginDraw. (À des fins d'optimisation, vous pouvez ajouter un paramètre pour un sous-rectangle avec la partie de l'image à mettre à jour.) De la même manière, la méthode EndDraw doit appeler ISurface­ImageSourceNative::EndDraw.

Les méthodes BeginDraw et EndDraw, dans ce cas, peuvent ressembler au code affiché à la figure 3.

Figure 3 Dessin sur la surface 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();
}

Notez que ma méthode BeginDraw accepte une primitive Rect comme entrée, qui est mappée à un type RECT natif. Ce type RECT définit la partie de l'écran dans laquelle je souhaite dessiner avec un élément SurfaceImageSource correspondant. BeginDraw, toutefois, ne peut être appelée qu'une fois à la fois ; je dois mettre en file d'attente les appels BeginDraw pour chaque SurfaceImageSource, l'un après l'autre.

Notez également que j'initialise une référence à un élément IDXGISurface et une structure POINT de décalage qui contient les coordonnées de décalage (x, y) du type RECT que je vais dessiner dans IDXGISurface par rapport à la partie supérieure gauche. Ce pointeur de surface et le décalage sont retournés depuis ISurfaceImageSourceNative::BeginDraw pour fournir l'élément IDXGISurface pour le dessin. Les appels ultérieurs de l'exemple créent une image bitmap à partir du pointeur de surface reçu et dessinent dans celle-ci avec des appels Direct2D. Quand ISurfaceImageSourceNative::EndDraw est appelée dans la surcharge EndDraw, le résultat final est une image terminée, une image disponible pour dessiner dans la primitive ou l'élément d'image XAML.

Regardons ce que nous avons :

  • Un type que j'ai dérivé de SurfaceImageSource.
  • Des méthodes sur mon type dérivé qui définissent son comportement de dessin pour un type RECT fourni à l'écran.
  • Les ressources graphiques DirectX nécessaires pour effectuer le dessin.
  • Une association entre le périphérique graphique DirectX et SurfaceImageSource.

Ce dont j'ai encore besoin :

  • Du code qui réalise le rendu d'image réel dans un type RECT.
  • Une connexion entre l'instance <Image> spécifique (ou primitive) dans XAML et l'instance SurfaceImageSource, que l'application appellera.

Je me charge du code relatif au comportement de dessin et il est probablement plus simple de l'implémenter sur mon type SurfaceImageSource en tant que méthode publique spécifique pouvant être appelée à partir du code-behind.

Le reste est simple. Dans le code-behind pour mon XAML, j'ajoute le code suivant à mon constructeur :

// 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;

Et j'ajoute un gestionnaire d'événements dans le même code-behind, comme ceci :

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();
}

(Sinon, si je ne dérive pas de SurfaceImageSource, je pourrais placer les appels à BeginDraw et EndDraw dans une méthode, telle que DrawTheMostAmazingSpinning3DShadedCubeEver de l'extrait de code précédent, sur mon objet de dessin.)

Maintenant, si j'utilise une primitive XAML, telle que Rect ou Ellipse, je crée un élément ImageBrush et lui associe SurfaceImageSource, comme ceci (où MySISPrimitive est une primitive graphique 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;

Voilà, c'est tout ! Pour résumer, le processus de mon exemple est le suivant :

  1. Choisir un élément de création d'images XAML, par exemple Image, ImageBrush ou une primitive graphique (Rect, Ellipse, etc.), dans lequel je souhaite effectuer un rendu. Déterminer également si la surface va fournir une image animée. Placer l'élément dans mon XAML.
  2. Créer le périphérique DirectX et les contextes de périphérique (en général Direct2D ou Direct3D, ou les deux) qui seront utilisés pour les opérations de dessin. Par ailleurs, à l'aide de COM, acquérir une référence à l'interface ISurfaceImageSourceNative qui sous-tend le type d'exécution SurfaceImageSource et lui associer le périphérique graphique.
  3. Créer un type qui dérive de SurfaceImageSource et qui contient le code qui appelle ISurfaceImageSource::BeginDraw et ISurfaceImageSource::EndDraw.
  4. Ajouter toutes les opérations de dessin spécifiques en tant que méthodes sur le type SurfaceImageSource.
  5. Pour les surfaces Image, connecter la propriété Source à une instance de type SurfaceImageSource. Pour les surfaces de primitives graphiques, créer un élément ImageBrush et affecter une instance SurfaceImage­Source à la propriété ImageSource, puis utiliser ce pinceau avec la propriété Fill de la primitive (ou toute propriété qui accepte un élément ImageSource ou ImageBrush).
  6. Appeler les opérations de dessin sur les instances SurfaceImageSource à partir des gestionnaires d'événements. Pour les images animées, vérifier que les opérations de dessin de trames sont interruptibles.

Je peux utiliser Surface­ImageSource pour des scénarios de jeux en 2D et 3D si la scène et les nuanceurs sont assez simples. Par exemple, un jeu de stratégie avec un niveau graphique moyen (pensez à « Civilization 4 ») ou un simple jeu de type « dungeon crawler » peut restituer les effets visuels dans un élément SurfaceImageSource.

Notez également que je peux créer le type SurfaceImageSource dérivé en C++ dans une DLL séparée et utiliser ce type à partir d'une autre projection de langage autre que C++. Dans ce cas, je peux limiter mon convertisseur et mes méthodes à C++, et créer mon infrastructure d'application et mes code-behind en C#, par exemple. Le modèle MVVM (Model-View-ViewModel) décide !

Ce qui nous limite :

  • Le contrôle qui affiche SurfaceImageSource est conçu pour les surfaces de taille fixe.
  • Le contrôle qui affiche SurfaceImageSource n'est pas optimisé pour les performances pour des surfaces arbitrairement importantes, en particulier les surfaces qui peuvent faire l'objet d'un zoom ou d'un panoramique dynamique.
  • L'actualisation de contrôle est gérée par le fournisseur d'affichage de l'infrastructure XAML WinRT et se produit lors de l'actualisation de l'infrastructure. Pour les scénarios graphiques haute fidélité et en temps réel, l'impact peut être notable sur les performances (en d'autres termes, cela ne convient pas vraiment à votre tout nouveau jeu de combat intergalactique gourmand en nuanceurs).

Cela nous amène à VirtualSurfaceImageSource (et, finalement, à SwapChainBackgroundPanel). Examinons le premier :

VirtualSurfaceImageSource et rendu de contrôle interactif

VirtualSurfaceImageSource est une extension de SurfaceImageSource, mais il est conçu pour les surfaces d'images qui peuvent être redimensionnées par l'utilisateur, en particulier les images dont la taille peut être supérieure à celle de l'écran ou qui peuvent être en partie déplacées hors de l'écran, ou encore qui peuvent être entièrement ou en partie masquées par d'autres images ou éléments XAML. Ce type fonctionne particulièrement bien avec les applications dans lesquelles l'utilisateur effectue régulièrement un panoramique ou un zoom d'une image qui peut être plus grande que l'écran, par exemple un contrôle de carte ou une visionneuse d'images.

Le processus pour VirtualSurfaceImageSource est identique à celui pour SurfaceImageSource tel qu'il a été présenté plus tôt ; seulement, vous utilisez le type VirtualSurfaceImageSource à la place de SurfaceImageSource et l'implémentation de l'interface IVirtualImageSourceNative à la place de celle d'ISurfaceImageSourceNative.

Cela signifie que je change mon code par rapport à l'exemple précédent pour :

  • Utiliser VirtualSurfaceImageSource à la place de SurfaceImage­Source. Dans les exemples de code suivants, je vais faire en sorte que ma classe de type source d'image de base, MyImageSourceType, dérive de VirtualSurfaceImageSource.
  • Rechercher l'implémentation de la méthode sur l'interface IVirtualSurfaceImageSourceNative sous-jacente.

Pour afficher un exemple, consultez la figure 4.

Figure 4 Héritage de 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));
  }
  // ...
}

Oh, il existe une autre différence très importante : je dois implémenter un rappel qui est appelé chaque fois qu'une « vignette » (une zone rectangulaire définie, à ne pas confondre avec les vignettes de l'interface utilisateur Windows 8) de la surface devient visible et doit être dessinée. Ces vignettes sont gérées par l'infrastructure lorsqu'une application crée une instance de VirtualSurfaceImageSource et vous ne contrôlez pas leurs paramètres. Au lieu de cela, en coulisses, une grande image est sous-divisée en ces vignettes et le rappel est appelé chaque fois qu'une partie de l'une de ces vignettes devient visible pour l'utilisateur et nécessite une mise à jour.

Pour utiliser ce mécanisme, je dois d'abord implémenter un type instanciable qui hérite de l'interface IVirtualSurfaceUpdatesCallbackNative et inscrire une instance de ce type en la passant à IVirtualSurfaceImageSource::RegisterForUpdatesNeeded, comme illustré à la figure 5.

Figure 5 Configuration d'un rappel pour 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);
  // ...
}

L'opération de dessin est implémentée sous la forme de la méthode UpdatesNeeded de l'interface IVirtualSurfaceUpdatesCallbackNative. Si une zone spécifique est devenue visible, je dois identifier les vignettes à mettre à jour. Pour ce faire, j'appelle IVirtualSurfaceImage­SourceNative::GetRectCount et, si le nombre de vignettes mises à jour est supérieur à zéro, j'obtiens les rectangles spécifiques pour ces vignettes mises à jour avec IVirtualSurfaceImageSourceNative::GetUpdateRects et j'effectue la mise à jour de chacun :

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 ...
  }
}

Je peux obtenir les paramètres définis par VirtualSurfaceImageSource pour ces vignettes en tant qu'objets RECT. Dans l'exemple précédent, j'obtiens un tableau d'objets RECT pour toutes les vignettes nécessitant des mises à jour. J'utilise ensuite les valeurs des RECT pour redessiner les vignettes en les fournissant à VirtualSurfaceImageSource::BeginDraw.

Encore une fois, comme avec SurfaceImageSource, j'initialise un pointeur vers IDXGISurface et j'appelle la méthode BeginDraw sur IVirtualSurfaceImageSourceNative (l'implémentation d'interface native sous-jacente) pour obtenir la surface actuelle dans laquelle dessiner. Le décalage, toutefois, fait référence au décalage (x, y) de l'objet RECT cible et non à l'élément d'image dans son intégralité.

Pour chaque objet RECT à mettre à jour, j'appelle un code similaire à celui de la figure 6.

Figure 6 Gestion des mises à jour de la visibilité ou de la taille du contrôle

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.
}

Encore une fois, je ne peux pas paralléliser ces appels, car il est possible d'effectuer uniquement une opération à la fois sur le thread d'interface utilisateur de l'interface graphique. Je peux traiter chaque RECT de vignette en série ou appeler IVirtualSurface­ImageSourceNative::BeginDraw avec une zone unie de tous les objets RECT pour une seule mise à jour de dessin. C'est le développeur qui décide.

Enfin, j'appelle IVirtualSurfaceImageSourceNative::EndDraw après avoir mis à jour chaque RECT de vignette modifié. Lorsque la dernière vignette mise à jour est traitée, je dispose d'une image bitmap entièrement à jour à fournir à la primitive ou à l'image XAML correspondante, comme je l'ai fait dans l'exemple SurfaceImageSource.

Voilà, c'est tout ! Cette forme d'interopérabilité DirectX-XAML est idéale lorsque les utilisateurs ne se soucient pas des entrées de latence faible pour les graphiques 3D en temps réel, comme cela peut être le cas dans un jeu en temps réel avec plus de détails. C'est également génial pour les contrôles et applications riches en graphiques ainsi que pour les jeux plus asynchrones (en d'autres termes au tour par tour).

Dans le prochain article, j'examinerai le revers de cette approche : le dessin XAML sur la chaîne de permutation DirectX et le travail nécessaire pour que l'infrastructure XAML fonctionne correctement avec un fournisseur d'affichage DirectX personnalisé. Restez connecté.

Doug Erickson est un rédacteur-programmeur expérimenté de Microsoft, qui travaille dans les services de contenu Windows et est spécialisé dans DirectX et le développement de jeux du Windows Store. Il espère que vous allez créer de nombreux jeux DirectX du Windows Store extraordinaires et que vous deviendrez célèbre.

Merci aux experts techniques suivants d'avoir relu cet article : Jesse Bishop et Bede Jordan