DirectX

Verwenden von XAML mit DirectX und C++ in Windows Store-Apps

Doug Erickson

 

Seit Windows Vista ist DirectX die wichtigste Grafik-API für die Windows-Plattform und ermöglicht die Beschleunigung des Grafikprozessors (Graphics Processing Unit, GPU) für alle Bildschirmzeichenoperationen des Betriebssystems. Vor Windows 8 mussten DirectX-Entwickler jedoch ihre eigenen Benutzeroberflächenframeworks von Grund auf in systemeigenem C++-Code und COM entwickeln oder ein Middleware-Benutzeroberflächenpaket wie Scaleform lizenzieren.

In Windows 8 können Sie die Lücke zwischen systemeigenem DirectX und einem geeigneten Benutzeroberflächenframework mit dem Windows-Runtime(WinRT)-Feature DirectX-XAML-Interop überbrücken. Sie müssen „systemeigenen“ C++-Code verwenden (obwohl Sie Zugriff auf intelligente Zeiger und die C++-Komponentenerweiterungen haben), um den API-Support für XAML in DirectX nutzen zu können. Ein paar Grundkenntnisse in COM können auch nützlich sein. Sie erhalten von mir allerdings eine vollständige Angabe des spezifischen Interops, das Sie ausführen müssen, um das XAML-Framework mit DirectX-Operationen zusammenbringen.

In dieser zweiteiligen Artikelreihe beleuchte ich die zwei Ansätze des DirectX-XAML-Interops: In dem einen Ansatz zeichne ich mit den DirectX-Grafik-APIs Flächen in XAML-Frameworkelemente, und in dem anderen Ansatz zeichne ich die XAML-Steuerelementhierarchie auf eine DirectX-Swapkettenfläche.

In diesem Artikel bespreche ich das erste Szenario, in dem im XAML-Framework angezeigte Bilder oder Grundtypen gerendert werden.

Zuerst erhalten Sie eine kurze Übersicht über die API-Optionen. Zurzeit gibt es in Windows-Runtime drei XAML-Typen, die das DirectX-Interop unterstützen:

  • „Windows::UI::Xaml::Media::Imaging::SurfaceImage­Source“ („SurfaceImageSource“ im Folgenden): Mit diesem Typ können Sie mithilfe von DirectX-Grafik-APIs relativ statischen Inhalt in eine freigegebene XAML-Fläche zeichnen. Die Ansicht wird vollständig von dem WinRT XAML-Framework verwaltet. Das bedeutet, dass alle Präsentationselemente auf die gleiche Weise verwaltet werden. Dies ist ideal zum Zeichnen von komplexen Inhalten, die sich nicht in jedem Frame ändern. Für komplexe 2D- oder 3D-Spiele, die häufig aktualisiert werden, ist der Ansatz jedoch nur bedingt geeignet.
  • „Windows::UI::Xaml::Media::Imaging::VirtualSurface­ImageSource“ („VirtualSurfaceImageSource“ im Folgenden): Wie SurfaceImageSource werden hier die für das XAML-Framework definierten Grafikressourcen verwendet. Im Gegensatz zu „SurfaceImage­Source“ unterstützt „VirtualSurfaceImageSource“ logisch große Flächen auf optimierte, bereichsbasierte Weise, sodass DirectX nur die Bereiche der Fläche zeichnet, die sich zwischen den Aktualisierungen ändern. Wählen Sie dieses Element, wenn Sie zum Beispiel ein Kartensteuerelement oder einen großen Dokument-Viewer mit hoher Bilddichte erstellen. Auch dieser Typ ist wie SurfaceImageSource nicht für komplexe 2D- oder 3D-Spiele geeignet, insbesondere solche Spiele, die auf Feedback und visuellen Elementen in Echtzeit beruhen.
  • „Windows::UI::Xaml::Controls::SwapChainBackgroundPanel“ („SwapChainBackgroundPanel“ im Folgenden): Dieses XAML-Steuerelement und dieser Typ ermöglichen Ihrer App die Verwendung eines benutzerdefinierten DirectX-Ansichtsanbieters (und einer Swapkette), auf den Sie XAML-Elemente zeichnen können. Zudem sorgt der Ansichtsanbieter in Szenarien, die eine Präsentation mit sehr niedriger Latenz oder Feedback mit hoher Frequenz erfordern (wie moderne Spiele), für eine bessere Leistung. Ihre App verwaltet unabhängig vom XAML-Framework den DirectX-Gerätekontext für „SwapChainBackgroundPanel“. Das bedeutet natürlich, dass „SwapChainBackgroundPanel“ und die XAML-Frames zur Aktualisierung nicht miteinander synchronisiert werden. Sie können auch von einem Hintergrundthread in „SwapChainBackgroundPanel“ rendern.

Dieses Mal beleuchte ich die SurfaceImageSource- und Virtual­SurfaceImageSource-APIs und ihre Integration in Ihre XAML-Steuerelemente mit umfassenden Bildern und Medien (für „SwapChainBackgroundPanel“ gelten andere Regeln, die in einem eigenen Artikel behandelt werden).

Anmerkung: „SurfaceImageSource“ und „VirtualSurfaceImageSource“ können von C# oder Visual Basic .NET verwendet werden. Die DirectX-Renderingkomponente muss jedoch in C++ geschrieben und in einer separaten DLL kompiliert werden, auf die vom C#-Projekt zugegriffen wird. Es gibt auch von Drittanbietern verwaltete WinRT-DirectX-Frameworks, wie „SharpDX“ (sharpdx.org) und „MonoGame“ (monogame.net), die Sie anstelle von „SurfaceImageSource oder „VirtualSurfaceImageSource“ verwenden können.

Fangen wir also an. In diesem Artikel wird davon ausgegangen, dass Sie die Grundlagen von DirectX beherrschen, insbesondere Direct2D, Direct3D und Microsoft DirectX Graphics Infrastructure (DXGI). Natürlich kennen Sie XAML und C++, dies sind Grundkenntnisse für Windows-App-Entwickler. So ausgerüstet können wir starten!

SurfaceImageSource und DirectX-Bildzusammensetzung

Der Windows::UI::Xaml::Media::Imaging-Namespace enthält neben vielen anderen XAML-Bildtypen den SurfaceImageSource-Typ. Der SurfaceImageSource-Typ bietet tatsächlich eine Methode, in die geteilten Flächen vieler XAML-Grafiken und -Bildgrundtypen dynamisch zu zeichnen, sie effektiv mit den Inhalten zu füllen, die Sie mit den DirectX-Grafikaufrufen rendern, und sie als Pinsel anzuwenden. (Genau gesagt, verwenden Sie „ImageSource“ als „ImageBrush“.) Betrachten Sie sie als Bitmap, die Sie direkt mit DirectX erzeugen. Denken Sie daran, dass Sie diesen Typ an vielen Stellen verwenden können, an denen Sie eine Bitmap oder eine andere Bildressource anwenden würden.

Für die Zwecke dieses Artikels zeichne ich in ein <Image>-XAML-Element, das ein leeres PNG-Bild als Platzhalter enthält. Ich gebe für das <Image>-Element eine Höhe und Breite an, da diese Informationen an den SurfaceImageSource-Konstruktor in meinem Code übergeben werden (wenn ich keine Höhe und Breite angebe, wird der gerenderte Inhalt gestreckt, um an die <Image>-Tagparameter angepasst zu werden):

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

In diesem Beispiel ist das <Image>-Tag mein Ziel. Es zeigt die Fläche an, in die ich zeichne. Ich könnte auch einen XAML-Grundtyp verwenden, wie <Rectangle> oder <Ellipse>. Beide können mit einem SurfaceImageSource-Pinsel gefüllt werden. Dies ist möglich, weil die Zeichnungen für diese Grundtypen und Bilder von Windows-Runtime mit DirectX ausgeführt werden. Ich verbinde lediglich im Hintergrund eine andere Renderingquelle.

In meinen Code schließe ich Folgendes ein:

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

Dies sind die Header für die Windows-Runtime-Bibliothek (Windows Runtime Library, WRL), einige wichtige DirectX-Komponenten und – am wichtigsten – die systemeigenen DirectX-Interopschnittstellen. Bald wird deutlich, dass Letzteres hinzugefügt werden muss.

Ich importiere auch die entsprechenden Bibliotheken: „dxgi.lib“, „d2d1.lib“ und „d3d11.lib“.

Aus praktischen Gründen füge ich zudem die folgenden Namespaces ein:

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

Im Code erstelle ich jetzt einen MyImageSourceType-Typ, der vom grundlegenden SurfaceImageSource-Typ erbt und seinen Konstruktor aufruft (siehe Abbildung 1).

Abbildung 1: Ableiten von „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();
  }
  // ...
}

Anmerkung: Sie müssen nicht von „SurfaceImageSource“ erben, auch wenn dadurch aus Codeorganisations-Per­spektive alles ein wenig einfacher wird. Sie können ganz unkompliziert ein SurfaceImageSource-Objekt als Member instanziieren und es stattdessen verwenden. Ersetzen Sie einfach in Ihrem Geiste in den Codebeispielen den Namen Ihres Members durch den Objekteigenverweis „(this)“.

Die CreateDeviceResources-Methode und die CreateDeviceIndependent­Resources-Methode sind Benutzerimplementierungen, mit denen bequem das für die DirectX-Grafikhardwareschnittstelle spezifische Setup und das allgemeinere DirectX-App-spezifische Setup logisch getrennt werden können. Die in beiden Methoden ergriffenen Maßnahmen sind von zentraler Bedeutung. Dennoch ist es praktisch (und erforderlich), sie zu trennen, denn möglicherweise möchten Sie einmal die Geräteressourcen ohne Auswirkungen auf die geräteunabhängigen Ressourcen neu erstellen und umgekehrt.

„CreateDeviceResources“ sollte in etwa wie der Code in Abbildung 2 aussehen, zumindest grundlegend.

Abbildung 2: Erstellen von gerätespezifischen Ressourcen für 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());
}

Jetzt habe ich einen Hardwaregerätekontext erstellt und ihn mit einem ... Moment mal, was ist eigentlich „ISurfaceImageSourceNative“? Das ist doch überhaupt kein WinRT-Typ! Was geht hier vor sich?

Das ist der Interopteil. An dieser Stelle schleiche ich mich in die „Jefferies-Röhre“ der WRL ein und nehme ein paar Umverdrahtungen vor. Hier gelange ich auch in das COM, das hinter einem großen Teil der WRL steht.

Um dieses Interopverhalten zu ermöglichen, muss ich in erster Linie mit der DirectX-Quelle im Hintergrund eine Verbindung herstellen. Hierfür ist es erforderlich, dass ich meinen Typ mit der Implementierung der Methoden verbinde, die in der WRL-spezifischen COM-Schnittstelle „ISurfaceImageSourceNative“ definiert sind. Anschließend hänge ich den Typ an das <Image>-Element (in diesem Beispiel) an. Wenn die App eine Aktualisierung am XAML-Framework auslöst, wird sie meine DirectX-Implementierungen des draw-Aufrufs anstatt der Standardimplementierungen verwenden.

„ISurfaceImageSourceNative“ wird im oben angegebenen Interopheader definiert. Erkennen Sie, was hier passiert?

In meiner App-spezifischen CreateDeviceIndependentResources-Methode trenne ich das COM ab und frage die systemeigenen Methoden ab, die in „SurfaceImageSource“ definiert sind. Da diese Methoden nicht direkt zur Verfügung gestellt werden, müssen Sie durch einen Aufruf von „IUnknown::Query­Interface“ im SurfaceImageSource-Typ oder im SurfaceImageSource-­abgeleiteten Typ abgerufen werden. Hierfür wandele ich meinen SurfaceImageSource-abgeleiteten Typ in „IUnknown“ um, die Basisschnittstelle für eine beliebige COM-Schnittstelle (ich könnte ihn auch in „IInspectable“ umwandeln, die „Basisschnittstelle“ für einen beliebigen WinRT-Typ, der von „IUnknown„ erbt). Anschließend frage ich die Schnittstelle folgendermaßen ab, um eine Liste der ISurfaceImageSourceNative-Methoden abzurufen:

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

(IID_PPV_ARGS ist ein Hilfsmakro für die WRL, die einen Schnittstellenzeiger abruft. Wie passend! Wenn Sie von „SurfaceImageSource„ nicht erben, ersetzen Sie den Membernamen Ihres SurfaceImageSource-Objekts dadurch.)

Schließlich macht dieser Teil der CreateDeviceResources-Methode Sinn:

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

„ISurfaceImageSourceNative::SetDevice“ bindet die konfigurierte Grafikschnittstelle an die Fläche beliebiger Zeichenoperationen. Das bedeutet auch, dass ich „Create­DeviceResources“ nach dem Aufrufen von „CreateDeviceIndependentResources“ mindestens einmal vorher aufrufen sollte. Andernfalls habe ich kein konfiguriertes Gerät zum Anhängen.

Ich habe jetzt die zugrunde liegende ISurfaceImageSourceNative-Implementierung des SurfaceImageSource-Typs zur Verfügung gestellt, von dem mein MyImageSourceType-Typ abgeleitet wird. Ich habe effektiv alles offengelegt und die Verbindung zum SurfaceImageSource-Typ gewechselt, allerdings zu der Basisimplementierung der draw-Aufrufe und nicht meinen eigenen. Jetzt implementiere ich meine Aufrufe.

Hierfür implementiere ich die folgenden Methoden:

  • „BeginDraw„ öffnet den Gerätekontext zum Zeichnen.
  • „EndDraw“ schließt den Gerätekontext.

Anmerkung: Ich habe die Methodennamen „BeginDraw“ und „EndDraw“ für eine Art lockerer Korrespondenz mit den ISurfaceImageSourceNative-Methoden gewählt. Dieses Muster besteht aus praktischen Gründen und ist nicht erzwungen.

Meine BeginDraw-Methode (oder eine andere draw-Initialisierungsmethode, die ich für den abgeleiteten Typ definiere) muss irgendwann „ISurface­ImageSourceNative::BeginDraw“ aufrufen. (Zur Optimierung können Sie einen Parameter für ein Unterrechteck mit dem zu aktualisierenden Bereich des Bilds hinzufügen.) Entsprechend sollte die EndDraw-Methode „ISurface­ImageSourceNative::EndDraw“ aufrufen.

Die BeginDraw- und die EndDraw-Methoden könnten in diesem Fall etwa wie der Code in Abbildung 3 aussehen.

Abbildung 3: Zeichnen in die DirectX-Fläche

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

Beachten Sie, dass meine BeginDraw-Methode einen Rect-Grundtyp als Eingabe verwendet, der einem systemeigenen RECT-Typ zugeordnet ist. Dieser RECT-Typ definiert den Bereich des Bildschirms, in den ich mit entsprechender „SurfaceImageSource“ zeichnen möchte. „BeginDraw“ kann jedoch nicht mehrmals gleichzeitig abgerufen werden. Ich muss die BeginDraw-Aufrufe für jede „SurfaceImageSource“ nacheinander in die Warteschlange stellen.

Beachten Sie auch, dass ich einen Verweis auf „IDXGISurface“ sowie eine Offset-POINT-Struktur mit den Koordinaten für den X-Y-Punkt-Offset des RECT initialisiere, das ich im Verhältnis zur linken oberen Ecke in „IDXGISurface“ zeichne. Der Oberflächenzeiger und -offset werden von „ISurfaceImageSourceNative::BeginDraw“ zurückgegeben, um „IDXGISurface“ zum Zeichnen zur Verfügung zu stellen. Zukünftige Aufrufe im Beispiel erstellen eine Bitmap aus dem empfangenen Oberflächenzeiger und zeichnen mit Direct2D-Aufrufen in sie. Wenn „ISurfaceImageSourceNative::EndDraw“ in der EndDraw-Überladung aufgerufen wird, ist ein vollständiges Bild das Endergebnis: ein Bild, das zum Zeichnen in das XAML-Bildelement oder den entsprechenden Grundtyp verfügbar ist.

Schauen wir uns einmal an, was ich bereits habe:

  • Einen von „SurfaceImageSource“ abgeleiteten Typ
  • Methoden für meinen abgeleiteten Typ, die das Zeichenverhalten für ein bereitgestelltes RECT auf dem Bildschirm definieren
  • Die für das Zeichnen benötigten DirectX-Grafikressourcen
  • Eine Zuordnung zwischen dem DirectX-Grafikgerät und „SurfaceImageSource“.

Ich benötige noch Folgendes:

  • Code, der das tatsächliche Bildrendering in ein RECT durchführt
  • Eine von der App aufzurufende Verbindung zwischen der spezifischen <Image>-Instanz (oder dem Grundtyp) im XAML und der SurfaceImageSource-Instanz

Der Code für das Zeichenverhalten ist mir überlassen. Am einfachsten lässt er sich wahrscheinlich in meinem SurfaceImageSource-Typ als spezifische öffentliche Methode implementieren, die vom Codebehind aufgerufen werden kann.

Der Rest ist einfach. Im Codebehind für mein XAML füge ich meinem Konstruktor diesen Code hinzu:

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

Zudem füge ich einen Ereignishandler im gleichen Codebehind hinzu, in etwa so:

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

(Wenn ich nicht aus „SurfaceImageSource“ ableite, könnte ich die Aufrufe von „BeginDraw“ und „EndDraw“ in einer Methode, wie „DrawTheMostAmazingSpinning3DShadedCubeEver“ aus dem vorherigen Codeausschnitt, in meinem Zeichenobjekt platzieren.

Wenn ich einen XAML-Grundtyp wie „Rect“ oder „Ellipse“ verwende, erstelle ich ein „ImageBrush“ und füge „SurfaceImageSource“ folgendermaßen daran (wobei „MySISPrimitive“ ein XAML-Grafikgrundtyp ist):

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

Und das ist auch schon alles! Im Folgenden fasse ich die Vorgehensweise in meinem Beispiel kurz zusammen:

  1. Ich wähle ein XAML-Bildelement wie „Image“, „ImageBrush“ oder einen Grafikgrundtyp („Rect“, „Ellipse“ usw.), in das ich rendern möchte. Ich bestimme auch, ob die Fläche ein animiertes Bild enthalten soll. Ich platziere es in mein XAML.
  2. Ich erstelle das DirectX-Gerät und den Gerätekontext (normalerweise „Direct2D“ oder „Direct3D“ oder beide) für die Zeichenoperationen. Darüber hinaus rufe ich mithilfe von COM einen Verweis auf die ISurfaceImageSourceNative-Schnittstelle ab, mit der die Grundlage für den SurfaceImageSource-Laufzeittyp gebildet wird, und verknüpfe sie mit dem Grafikgerät.
  3. Dann erstelle ich einen Typ, der von „SurfaceImageSource“ abgeleitet wird und dessen Code „ISurfaceImageSource::BeginDraw“ und „ISurfaceImageSource::EndDraw“ aufruft.
  4. Ich füge dem SurfaceImageSource-Typ beliebige spezifische Zeichenoperationen als Methoden hinzu.
  5. Für Image-Flächen verbinde ich die Source-Eigenschaft mit einer SurfaceImageSource-Typinstanz. Für Grafikgrundtypen-Flächen erstelle ich ein „ImageBrush“ und weise der ImageSource-Eigenschaft eine SurfaceImage­Source-Instanz zu. Anschließend verwende ich das „Brush“ mit der Fill-Grundlageneigenschaft (oder einer beliebigen Eigenschaft, die „ImageSource“ oder „ImageBrush“ akzeptiert).
  6. Schließlich rufe ich die Zeichenoperationen in den SurfaceImageSource-Instanzen aus Ereignishandlern auf. Bei animierten Bildern stelle ich sicher, dass die Framezeichenoperationen unterbrochen werden können.

Ich kann „Surface­ImageSource“ für 2D- und 3D-Spiele verwenden, wenn die Szene und die Shader einfach gestaltet sind. Ein grafisch durchschnittliches Strategiespiel (wie „Civilization 4“) oder ein einfaches Dungeon Crawler könnten die Darstellungselemente in „SurfaceImageSource“ rendern.

Beachten Sie, dass ich den abgeleiteten SurfaceImageSource-Typ in C++ in einer separaten DLL erstellen kann und ihn aus einer anderen Sprachprojektion als C++ in verwenden kann. In diesem Fall könnte ich den Renderer und die Methoden auf C++ beschränken und meine App-Infrastruktur und die Codebehinds z. B. in C# speichern. Regeln für Model-View-ViewModel (MVVM)!

Dies bringt uns zu den Einschränkungen:

  • Das Steuerelement, das „SurfaceImageSource“ anzeigt, wurde für Flächen mit festen Größen konzipiert.
  • Die Leistung des Steuerelements, das „SurfaceImageSource“ anzeigt, wurde nicht für beliebig große Flächen optimiert, insbesondere nicht für Flächen, die dynamisch geschwenkt oder gezoomt werden können.
  • Die Aktualisierung des Steuerelements wird vom Ansichtsanbieter für das WinRT XAML-Framework vorgenommen und wird bei der Frameworkaktualisierung durchgeführt. In Echtzeitszenarien mit hoher grafischer Wiedergabetreue können die Auswirkungen auf die Leistung deutlich feststellbar sein (damit meine ich, dass dies für Ihr neuestes shaderintensives intergalaktisches Kampfspiel nicht besonders gut geeignet ist).

Dies bringt uns zu „VirtualSurfaceImageSource“ (und schließlich zu „SwapChainBackgroundPanel“). Betrachten wir Ersteres.

„VirtualSurfaceImageSource“ und interaktives Steuerelementrendering

„VirtualSurfaceImageSource“ ist eine Erweiterung von „SurfaceImageSource“, wurde aber für Bildflächen entworfen, deren Größe vom Benutzer geändert werden kann. Das betrifft besonders Bilder, deren Größe über den Bildschirm hinausgehen kann, die sich teilweise außerhalb des Bildschirms befinden oder die von anderen Bildern oder XAML-Elementen teilweise oder ganz verdeckt werden. „VirtualSurfaceImageSource“ ist besonders für Apps geeignet, in denen der Benutzer regelmäßig ein Bild schwenkt oder zoomt, das potenziell größer als der Bildschirm ist, zum Beispiel ein Kartensteuerelement oder einen Image Viewer.

Der Prozess für „VirtualSurfaceImageSource“ ist mit dem oben beschriebenen für „SurfaceImageSource“ identisch. Der einzige Unterschied besteht darin, dass der VirtualSurfaceImageSource-Typ anstelle von „SurfaceImageSource“ und die IVirtualImageSourceNative-Schnittstellenimplementierung anstelle der ISurfaceImageSourceNative-Implementierung verwendet wird.

Das bedeutet, dass ich den Code aus dem vorherigen Beispiel folgendermaßen ändere:

  • Ich verwende „VirtualSurfaceImageSource“ anstelle von „SurfaceImage­Source“. In den folgenden Codebeispielen leitet meine grundlegende Bildquellen-Typklasse „MyImageSourceType“ von „VirtualSurfaceImageSource“ ab.
  • Ich frage die Methodenimplementierung der zugrunde liegenden IVirtualSurfaceImageSourceNative-Schnittstelle ab.

Ein Beispiel finden Sie in Abbildung 4.

Abbildung 4: Vererbung aus „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, und es gibt noch einen weiteren sehr wichtigen Unterschied: Ich muss einen Rückruf implementieren, der immer aufgerufen wird, wenn eine „Kachel“ (ein definierter rechteckiger Bereich, nicht mit den Kacheln der Benutzeroberfläche von Windows 8 zu verwechseln) der Fläche sichtbar wird und gezeichnet werden muss. Diese Kacheln werden vom Framework verwaltet, wenn eine App eine Instanz von „VirtualSurfaceImageSource“ erstellt und Sie die Parameter nicht steuern. Im Hintergrund ist stattdessen ein großes Bild in diese Kacheln unterteilt. Der Rückruf wird immer aufgerufen, wenn ein Teil einer dieser Kacheln für den Benutzer sichtbar wird und aktualisiert werden muss.

Zur Verwendung des Mechanismus muss ich zuerst einen instanziierbaren Typ implementieren, der von der IVirtualSurfaceUpdatesCallbackNative-Schnittstelle erbt. Außerdem muss ich eine Instanz des Typs registrieren, indem ich sie an „IVirtualSurfaceImageSource::RegisterForUpdatesNeeded“ übergebe (siehe Abbildung 5).

Abbildung 5: Einrichten eines Rückrufs für „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);
  // ...
}

Die Zeichenoperation wird als UpdatesNeeded-Methode von der IVirtualSurfaceUpdatesCallbackNative-Schnittstelle implementiert. Wenn ein angegebener Bereich sichtbar geworden ist, muss ich festlegen, welche Kacheln aktualisiert werden sollen. Hierfür rufe ich „IVirtualSurfaceImage­SourceNative::GetRectCount“ auf. Wenn die Anzahl von aktualisierten Kacheln größer als null ist, werden die bestimmten Rechtecke für die aktualisierten Kacheln mit „IVirtualSurfaceImageSourceNative::GetUpdateRects“ abgerufen und aktualisiert:

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

Ich kann die VirtualSurfaceImageSource-definierten Parameter für die Kacheln als RECT-Objekte abrufen. Im vorhergehenden Beispiel rufe ich ein Array von RECT-Objekten für alle Kacheln ab, die aktualisiert werden müssen. Dann verwende ich die Werte für die RECTs, um die Kacheln neu zu zeichnen, indem ich sie für „VirtualSurfaceImageSource::BeginDraw“ bereitstelle.

Wie auch mit „SurfaceImageSource“ initialisiere ich einen Zeiger auf „IDXGISurface“. Dann rufe ich die BeginDraw-Methode für „IVirtualSurfaceImageSourceNative“ auf (die zugrunde liegende systemeigene Schnittstellenimplementierung), um die aktuelle Fläche zum Zeichnen abzurufen. Der Offset bezieht sich jedoch auf den X-Y-Punkt-Offset für das Ziel-RECT und nicht auf das Bildelement als Ganzes.

Ich rufe für jedes zu aktualisierende RECT einen Code auf, der wie in Abbildung 6 aussieht.

Abbildung 6: Behandeln von Updates an der Größe des Steuerelements oder an der Sichtbarkeit

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

Erneut kann ich die Aufrufe nicht parallelisieren, weil an der Grafikschnittstelle nicht mehrere Operationen für den UI-Thread gleichzeitig vorhanden sein können. Ich kann die Kachel-RECTs nacheinander verarbeiten, oder ich kann „IVirtualSurface­ImageSourceNative::BeginDraw“ mit einem vereinten Bereich aller RECTs für ein einzelnes draw-Update aufrufen. Das muss der Entwickler entscheiden.

Schließlich rufe ich „IVirtualSurfaceImageSourceNative::EndDraw“ auf, nachdem ich jedes geänderte Kachel-RECT-aktualisiert habe. Wenn die letzte aktualisierte Kachel verarbeitet ist, besitze ich eine vollständig aktualisierte Bitmap, die dem entsprechenden XAML-Bild oder -Grundtyp zur Verfügung gestellt wird, wie auch in dem SurfaceImageSource-Beispiel.

Und das ist auch schon alles! Diese Form von DirectX-XAML-Interop ist nützlich, wenn für Benutzer die geringe Latenz für die Eingabe bei 3D-Grafiken in Echtzeit nicht so relevant ist wie in einem detaillierten Spiel in Echtzeit. Sie beeindruckt auch bei Apps und Steuerelementen mit umfassenden Grafiken und eher asynchronen (also rundenbasierten) Spielen.

Im folgenden Artikel werde ich die Schattenseite dieses Ansatzes beleuchten: Das Zeichnen von XAML auf die DirectX-Swapkette und der erforderliche Aufwand, damit das XAML-Framework mit einem benutzerdefinierten DirectX-Ansichtsprovider harmoniert. Bleiben Sie am Ball!

Doug Erickson aarbeitet bei Microsoft als leitender Redakteur im Bereich Programmierung. Er ist für Windows Content Services tätig und auf DirectX und die Spieleentwicklung für den Windows Store spezialisiert. Er hofft, dass Sie viele erstklassige Windows Store-DirectX-Spiele erstellen und berühmt werden.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Jesse Bishop und Bede Jordan