Windows mit C++

Rendering für Windows-Runtime

Kenny Kerr

Kenny KerrIn meinem letzten Artikel habe ich das Windows-Runtime-Anwendungsmodell (WinRT) erklärt (msdn.microsoft.com/magazine/dn342867). Ich habe gezeigt, wie Sie Windows Store- und Windows Phone-Apps mit Standard-C++ und klassischem COM schreiben und dazu nur wenige WinRT-API-Funktionen benötigen. Die Verwendung einer Sprachprojektion wie C++/CX oder C# ist ganz sicher nicht obligatorisch. Die Fähigkeit, diese Abstraktionen umgehen zu können, ist äußerst hilfreich und bietet gleichzeitig eine gute Gelegenheit, die Funktionsweise dieser Technologie zu verstehen.

In meinem Artikel von Mai 2013 habe ich Direct2D 1.1 vorgestellt und erläutert, wie Sie damit in einer Desktopanwendung rendern (msdn.microsoft.com/magazine/dn198239). Im nachfolgenden Beitrag ging es um die dx.h-Bibliothek, die unter dx.codeplex.com verfügbar ist und eine DirectX-Programmierung in C++ wesentlich einfacher macht (msdn.microsoft.com/magazine/dn201741).

Der Code in meinem letzten Artikel reichte aus, um die auf CoreWindow basierende App zum Leben zu erwecken, aber er bot kein Rendering.

In diesem Monat zeige ich Ihnen, wie Sie anhand dieses Grundgerüsts Unterstützung für Rendering hinzufügen. Das Anwendungsmodell WinRT wurde für das Rendering mit DirectX optimiert. Ich werde darstellen, wie Sie das in meinen vorherigen Artikeln über Direct2D- und Direct3D-Rendering Gelernte auf Ihre CoreWindow-basierte WinRT-App anwenden – genauer gesagt, wie Sie dazu Direct2D 1.1 mithilfe der dx.h-Bibliothek verwenden. Zum großen Teil sind die zu erstellenden Direct2D- und Direct3D-Zeichenbefehle dieselben, unabhängig davon, ob Sie die Befehle für die Desktop- oder die Windows-Runtime-Umgebung definieren. Es gibt jedoch kleinere Unterschiede, und das anfängliche Einbinden der Befehle ist auf jeden Fall anders. Ich mache da weiter, wo ich das letzte Mal aufgehört habe, und erörtere, wie Sie Pixel auf den Bildschirm bringen!

Damit Rendering ordnungsgemäß unterstützt wird, muss das Fenster bestimmte Ereignisse kennen. Dazu gehören mindestens Änderungen an der Sichtbarkeit und der Größe des Fensters sowie Änderungen an der vom Benutzer ausgewählten DPI-Konfiguration für die logische Anzeige. Wie bei dem im letzten Artikel besprochenen Activated-Ereignis werden diese neuen Ereignisse der Anwendung über COM-Schnittstellenrückrufe gemeldet. Die ICoreWindow-Schnittstelle stellt Methoden zum Registrieren der Ereignisse „VisibilityChanged“ und „SizeChanged“ bereit, aber zunächst muss ich die entsprechenden Handler implementieren. Die beiden zu implementierenden COM-Schnittstellen ähneln dem Activated-Ereignishandler mit seinen per MIDL (Microsoft Interface Definition Language) generierten Klassenvorlagen:

typedef ITypedEventHandler<CoreWindow *, VisibilityChangedEventArgs *>
  IVisibilityChangedEventHandler;
typedef ITypedEventHandler<CoreWindow *, WindowSizeChangedEventArgs *>
  IWindowSizeChangedEventHandler;

Als Nächstes muss die COM-Schnittstelle „IDisplayPropertiesEventHandler“ implementiert werden, die glücklicherweise schon definiert wurde. Ich integriere einfach die entsprechende Headerdatei:

#include <Windows.Graphics.Display.h>

Darüber hinaus sind die relevanten Typen im folgenden Namespace definiert:

using namespace ABI::Windows::Graphics::Display;

Dank dieser Definitionen kann ich die SampleWindow-Klasse aus meinem letzten Artikel aktualisieren, damit sie auch von diesen drei Schnittstellen erbt:

struct SampleWindow :
  ...
  IVisibilityChangedEventHandler,
  IWindowSizeChangedEventHandler,
  IDisplayPropertiesEventHandler

Ich darf nicht vergessen, meine QueryInterface-Implementierung mit der Unterstützung für diese Schnittstellen zu aktualisieren. Diesen Schritt überlasse ich Ihnen. Wie ich bereits letztes Mal erwähnt habe, ist es in Windows-Runtime unerheblich, wo diese COM-Schnittstellenrückrufe implementiert werden. Folglich geht Windows-Runtime nicht davon aus, dass das für meine App gültige IFrameworkView (die von der SampleWindow-Klasse implementierte primäre Schnittstelle) diese Schnittstellenrückrufe ebenfalls implementiert. Also handhabt „QueryInterface“ Abfragen für diese Schnittstellen zwar ordnungsgemäß, aber Windows-Runtime initiiert diese Abfragen nicht. Stattdessen muss ich die Registrierung für die entsprechenden Ereignisse durchführen, und das geschieht am besten im Rahmen der Implementierung der IFrameworkView-Methode „Load“. Zur Erinnerung: Die Load-Methode ist die ideale Stelle, um Code einzufügen und Ihre App für die erste Präsentation vorzubereiten. Anschließend kann ich die Ereignisse „VisibilityChanged“ und „SizeChanged“ innerhalb der Load-Methode registrieren:

EventRegistrationToken token;
HR(m_window->add_VisibilityChanged(this, &token));
HR(m_window->add_SizeChanged(this, &token));

Dadurch wird Windows-Runtime explizit mitgeteilt, wo die beiden ersten Schnittstellenimplementierungen zu finden sind. Die dritte und letzte Schnittstelle bezieht sich auf das LogicalDpiChanged-Ereignis, doch dieses Ereignis wird durch die IDisplayPropertiesStatics-Schnittstelle registriert. Diese statische Schnittstelle wird von der WinRT-Klasse „DisplayProperties“ implementiert. Ich beschaffe sie mir einfach mithilfe der GetActivationFactory-Funktionsvorlage (mehr zur Implementierung von „GetActivationFactory“ in meinem letzten Beitrag):

ComPtr<IDisplayPropertiesStatics> m_displayProperties;
m_displayProperties = GetActivationFactory<IDisplayPropertiesStatics>(
  RuntimeClass_Windows_Graphics_Display_DisplayProperties);

Die Membervariable speichert diesen Schnittstellenzeiger, da ich ihn zu verschiedenen Zeitpunkten im Lebenszyklus des Fensters aufrufen muss. Vorläufig registriere ich einfach das LogicalDpiChanged-Ereignis innerhalb der Load-Methode:

HR(m_displayProperties->add_LogicalDpiChanged(this, &token));

Auf die Implementierung dieser drei Schnittstellen gehe ich gleich noch einmal ein. Nun ist es an der Zeit, die DirectX-Infrastruktur vorzubereiten. Ich benötige den Standardsatz der Geräteressourcenhandler, die ich in früheren Artikeln bereits vielfach erläutert habe:

void CreateDeviceIndependentResources() {}
void CreateDeviceSizeResources() {}
void CreateDeviceResources() {}
void ReleaseDeviceResources() {}

Mit dem ersten Handler kann ich alle Ressourcen erstellen oder laden, die sich nicht speziell auf das zugrunde liegende Direct3D-Renderinggerät beziehen. Die nächsten beiden Handler dienen zum Erstellen gerätespezifischer Ressourcen. Am besten trennen Sie Ressourcen, die sich speziell auf die Fenstergröße beziehen, von anderen Ressourcen. Zum Schluss müssen alle Geräteressourcen freigegeben werden. Die verbleibende DirectX-Infrastruktur beruht darauf, dass die App diese vier Methoden ordnungsgemäß auf der Basis der spezifischen App-Anforderungen implementiert. Für die Verwaltung, die effiziente Erstellung und die Wiederverwendung von Renderingressourcen werden in der App separate Punkte bereitgestellt.

Jetzt kann ich dx.h den größten Teil der DirectX-Arbeit überlassen:

#include "dx.h"

Jede Direct2D-App beginnt mit der Direct2D-Factory:

Factory1 m_factory;

Sie finden sie im Direct2D-Namespace, und in der Regel integriere ich sie folgendermaßen:

using namespace KennyKerr;
using namespace KennyKerr::Direct2D;

Die dx.h-Bibliothek enthält separate Namespaces für Direct2D, Direct­Write, Direct3D, Microsoft DirectX Graphics Infrastructure (DXGI) usw. In den meisten meiner Apps wird Direct2D häufig verwendet, daher ist dieser Ansatz für mich zweckmäßig. Sie können die Namespaces natürlich auf jede beliebige Art verwalten, die für Ihre App sinnvoll ist.

Die Membervariable „m_factory“ stellt die Direct2D 1.1-Factory dar. Sie wird verwendet, um das Renderziel und eine Vielzahl anderer geräteunabhängiger Ressourcen nach Bedarf zu erstellen. Zuerst generiere ich die Direct2D-Factory und lasse dann die Erstellung von geräteunabhängigen Ressourcen im letzten Schritt der Load-Methode zu:

m_factory = CreateFactory();
CreateDeviceIndependentResources();

Nachdem die Load-Methode zurückgegeben hat, ruft die WinRT-Klasse „CoreApplication“ sofort die IFrameworkView-Methode „Run“ auf.

Die Implementierung der SampleWindow Run-Methode aus meinem letzten Beitrag hat zum Sperren einfach die ProcessEvents-Methode im CoreWindow-Verteiler aufgerufen. Eine solche Sperrung ist ausreichend, wenn Ihre App nur relativ selten Renderingvorgänge basierend auf verschiedenen Ereignissen ausführt. Vielleicht implementieren Sie ein Spiel oder benötigen einfach eine Animation mit hoher Auflösung für Ihre App. Das andere Extrem ist die Verwendung einer kontinuierlichen Animationsschleife. Aber Sie suchen möglicherweise nach einer intelligenteren Lösung. Ich implementiere hier eine Art Kompromiss zwischen diesen beiden Methoden. Als erstes füge ich eine Membervariable hinzu, die nachverfolgt, ob das Fenster sichtbar ist. So kann ich die Renderingvorgänge reduzieren, wenn das Fenster dem Benutzer nicht angezeigt wird:

bool m_visible;
SampleWindow() : m_visible(true) {}

Dann kann ich die Run-Methode wie in Abbildung 1 dargestellt neu schreiben.

Abbildung 1: Dynamische Renderingschleife

auto __stdcall Run() -> HRESULT override
{
  ComPtr<ICoreDispatcher> dispatcher;
  HR(m_window->get_Dispatcher(dispatcher.GetAddressOf()));
  while (true)
  {
    if (m_visible)
    {
      Render();
      HR(dispatcher->
        ProcessEvents(CoreProcessEventsOption_ProcessAllIfPresent));
    }
    else
    {
      HR(dispatcher->
        ProcessEvents(CoreProcessEventsOption_ProcessOneAndAllPending));
    }
  }
  return S_OK;
}

Die Run-Methode ruft wie zuvor den CoreWindow-Verteiler ab. Dann startet sie eine Endlosschleife, wobei sie alle Fenstermeldungen (die von Windows-Runtime „Ereignisse“ genannt werden) aus der Warteschlange rendert und verarbeitet. Wenn das Fenster jedoch nicht sichtbar ist, wird die Methode gesperrt, bis eine Meldung eingeht. Woran erkennt die App, dass sich die Sichtbarkeit des Fensters geändert hat? Dafür gibt es die IVisibilityChangedEventHandler-Schnittstelle. Ich kann nun die Invoke-Methode implementieren, um die Membervariable „m_visible“ zu aktualisieren:

auto __stdcall Invoke(ICoreWindow *,
  IVisibilityChangedEventArgs * args) -> HRESULT override
{
  unsigned char visible;
  HR(args->get_Visible(&visible));
  m_visible = 0 != visible;
  return S_OK;
}

Die per MIDL generierte Schnittstelle verwendet ein Zeichen vom Typ „unsigned char“ als portablen Boolean-Datentyp. Ich rufe die aktuelle Sichtbarkeit des Fensters einfach mithilfe des bereitgestellten IVisibilityChangedEventArgs-Schnittstellenzeigers ab und aktualisiere dann die Membervariable entsprechend. Dieses Ereignis wird ausgelöst, wenn das Fenster ausgeblendet oder angezeigt wird. Diese Vorgehensweise ist ein bisschen einfacher als die Implementierung für Desktopanwendungen, da sie für verschiedene Szenarien verwendet werden kann, beispielsweise zum Beenden von Apps oder für die Energieverwaltung – und natürlich zum Wechseln zwischen Fenstern.

Als Nächstes muss ich die Render-Methode implementieren, die von der Run-Methode in Abbildung 1 aufgerufen wird. Hier wird der Renderingstapel erstellt, und zwar sowohl nach Bedarf als auch beim Auftreten der eigentlichen Zeichenbefehle. Das Grundgerüst ist in Abbildung 2 zu sehen.

Abbildung 2: Übersicht über die Render-Methode

void Render()
{
  if (!m_target)
  {
    // Prepare render target ...
  }
  m_target.BeginDraw();
  Draw();
  m_target.EndDraw();
  auto const hr = m_swapChain.Present();
  if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr)
  {
    ReleaseDevice();
  }
}

Die Render-Methode sollte Ihnen bekannt vorkommen. Sie entspricht im Grunde der Form, die ich zuvor für Direct2D 1.1 erläutert habe. Zunächst wird das Renderziel nach Bedarf erstellt. Darauf folgen sofort die eigentlichen Zeichenbefehle zwischen den Aufrufen von „BeginDraw“ und „EndDraw“. Da es sich beim Renderziel um einen Direct2D-Gerätekontext handelt, muss zum Anzeigen der gerenderten Pixel auf dem Bildschirm die Swapkette präsentiert werden. Wo wir gerade dabei sind – ich muss sowohl die dx.h-Typen hinzufügen, die den Direct2D 1.1-Gerätekontext darstellen, als auch die DirectX 11.1-Version der Swapkette. Letztere befindet sich im Dxgi-Namespace:

DeviceContext m_target;
Dxgi::SwapChain1 m_swapChain;

Zum Abschluss der Render-Methode wird „ReleaseDevice“ aufgerufen, falls die Präsentation fehlschlägt:

void ReleaseDevice()
{
  m_target.Reset();
  m_swapChain.Reset();
  ReleaseDeviceResources();
}

So werden das Renderziel und die Swapkette freigegeben. Außerdem wird „ReleaseDeviceResources“ aufgerufen, damit alle gerätespezifischen Ressourcen freigegeben werden können, beispielsweise Pinsel, Bitmaps und Effekte. Die ReleaseDevice-Methode mag irrelevant erscheinen, aber sie ist wichtig, damit ein Geräteverlust in einer DirectX-App zuverlässig gehandhabt wird. Wenn nicht alle Geräteressourcen (also alle von der GPU unterstützten Ressourcen) ordnungsgemäß freigegeben wurden, kann Ihre App nach einem Geräteverlust nicht wiederhergestellt werden und stürzt ab.

Als Nächstes muss ich das Renderziel vorbereiten – diesen Schritt hatte ich in der Render-Methode in Abbildung 2 ausgelassen. Zuerst wird das Direct3D-Gerät erstellt. (Die dx.h-Bibliothek vereinfacht die nächsten Schritte erheblich.)

auto device = Direct3D::CreateDevice();

Jetzt ist das Direct3D-Gerät fertig, und ich kann mich der Direct2D-Factory zuwenden, um das Direct2D-Gerät und den Direct2D-Gerätekontext zu erstellen:

m_target = m_factory.CreateDevice(device).CreateDeviceContext();

Anschließend muss die Swapkette für das Fenster erstellt werden. Ich rufe zunächst die DXGI-Factory vom Direct3D-Gerät ab:

auto dxgi = device.GetDxgiFactory();

Dann erstelle ich eine Swapkette für das CoreWindow der Anwendung:

m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());

Auch hier macht die dx.h-Bibliothek die Sache viel einfacher, da sie die DXGI_SWAP_CHAIN_DESC1-Struktur automatisch ausfüllt. Dann rufe ich die CreateDeviceSwapChainBitmap-Methode auf, um eine Direct2D-Bitmap zu erstellen, die den Hintergrundpuffer der Swapkette darstellt:

void CreateDeviceSwapChainBitmap()
{
  BitmapProperties1 props(BitmapOptions::Target | BitmapOptions::CannotDraw,
    PixelFormat(Dxgi::Format::B8G8R8A8_UNORM, AlphaMode::Ignore));
  auto bitmap =
    m_target.CreateBitmapFromDxgiSurface(m_swapChain, props);
  m_target.SetTarget(bitmap);
}

Als erstes muss diese Methode den Hintergrundpuffer der Swapkette so beschreiben, dass Direct2D die Angaben versteht. „BitmapProperties1“ ist die dx.h-Version der D2D1_BITMAP_PROPERTIES1-Struktur für Direct2D. Die Konstante „BitmapOptions::Target“ gibt an, dass die Bitmap als Ziel für einen Gerätekontext verwendet wird. Die Konstante „Bitmap­Options::CannotDraw“ bezieht sich auf die Tatsache, dass der Hintergrundpuffer einer Swapkette nur als Ausgabe und nicht zur Eingabe für andere Zeichenoperationen verwendet werden kann. „PixelFormat“ ist die dx.h-Version der D2D1_PIXEL_FORMAT-Struktur für Direct2D.

Nun da die Bitmapeigenschaften definiert sind, ruft die CreateBitmapFromDxgiSurface-Methode den Hintergrundpuffer der Swapkette ab und erstellt eine Direct2D-Bitmap für die Darstellung. Auf diese Weise kann der Direct2D-Gerätekontext direkt in die Swapkette gerendert werden, indem die Bitmap über die SetTarget-Methode einfach als Ziel ausgewählt wird.

Zurück zur Render-Methode – nun muss Direct2D angewiesen werden, wie Zeichenbefehle in Übereinstimmung mit der DPI-Konfiguration des Benutzers skaliert werden:

float dpi;
HR(m_displayProperties->get_LogicalDpi(&dpi));
m_target.SetDpi(dpi);

Dann rufe ich die Geräteressourcenhandler der App auf, damit sie Ressourcen nach Bedarf erstellen. Als Zusammenfassung stellt Abbildung 3 die komplette Geräteinitialisierungssequenz für die Render-Methode dar.

Abbildung 3: Vorbereiten des Renderziels

void Render()
{
  if (!m_target)
  {
    auto device = Direct3D::CreateDevice();
    m_target = m_factory.CreateDevice(device).CreateDeviceContext();
    auto dxgi = device.GetDxgiFactory();
    m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());
    CreateDeviceSwapChainBitmap();
    float dpi;
    HR(m_displayProperties->get_LogicalDpi(&dpi));
    m_target.SetDpi(dpi);
    CreateDeviceResources();
    CreateDeviceSizeResources();
  }
  // Drawing and presentation ... see Figure 2

Zwar wird die DPI-Skalierung sofort nach dem Erstellen des Direct2D-Gerätekontexts ordnungsgemäß angewendet, aber sie muss stets aktualisiert werden, wenn der Benutzer diese Einstellung ändert. Die Tatsache, dass die DPI-Skalierung für eine App während der Ausführung geändert werden kann, ist neu in Windows 8. Nun kommt die IDisplayPropertiesEventHandler-Schnittstelle ins Spiel. Ich kann jetzt einfach die Invoke-Methode implementieren und das Gerät entsprechend aktualisieren. Hier sehen Sie den LogicalDpiChanged-Ereignishandler:

auto __stdcall Invoke(IInspectable *) -> HRESULT override
{
  if (m_target)
  {
    float dpi;
    HR(m_displayProperties->get_LogicalDpi(&dpi));
    m_target.SetDpi(dpi);
    CreateDeviceSizeResources();
    Render();
  }
  return S_OK;
}

Unter der Annahme, dass das Ziel – der Gerätekontext – erstellt wurde, wird der aktuelle logische DPI-Wert abgerufen und einfach an Direct2D weitergeleitet. Dann wird die App aufgerufen, damit sie vor dem erneuten Rendern alle gerätespezifischen Ressourcen neu erstellt. Auf diese Weise kann die App auf Änderungen an der DPI-Konfiguration der Anzeige dynamisch reagieren. Als letzte dynamische Änderung muss das Fenster Änderungen an der Fenstergröße umsetzen. Ich habe die Ereignisregistrierung bereits verknüpft, daher brauche ich nur die Implementierung der IWindowSizeChangedEventHandler Invoke-Methode hinzufügen, die den SizeChanged-Ereignishandler darstellt:

auto __stdcall Invoke(ICoreWindow *,
  IWindowSizeChangedEventArgs *) -> HRESULT override
{
  if (m_target)
  {
    ResizeSwapChainBitmap();
    Render();
  }
  return S_OK;
}

Jetzt muss ich nur noch die Größe der Swapkettenbitmap mithilfe der ResizeSwapChainBitmap-Methode ändern. Auch hier muss mit Sorgfalt gearbeitet werden. Die Größe der Puffer einer Swapkette zu ändern kann und sollte ein effizienter Vorgang sein, aber nur, wenn er richtig ausgeführt wird. Damit dieser Schritt erfolgreich ist, muss ich zunächst sicherstellen, dass alle Verweise auf diese Puffer freigegeben wurden. Die App enthält möglicherweise direkte oder indirekte Verweise. In diesem Fall ist der Verweis im Direct2D-Gerätekontext gespeichert. Das Zielbild ist die Direct2D-Bitmap, die ich zum Umschließen des Hintergrundpuffers der Swapkette erstellt habe. Die Freigabe ist erstaunlich einfach:

m_target.SetTarget();

Dann kann ich die ResizeBuffers-Methode der Swapkette aufrufen, damit sie den Großteil der Arbeit übernimmt, und anschließend rufe ich die Geräteressourcenhandler der App nach Bedarf auf. In Abbildung 4 sehen Sie das Ergebnis.

Abbildung 4: Ändern der Swapkettengröße

void ResizeSwapChainBitmap()
{
  m_target.SetTarget();
  if (S_OK == m_swapChain.ResizeBuffers())
  {
    CreateDeviceSwapChainBitmap();
    CreateDeviceSizeResources();
  }
  else
  {
    ReleaseDevice();
  }
}

Nun können Sie Zeichenbefehle hinzufügen, die dann von DirectX effizient in das CoreWindow-Ziel gerendert werden. Als einfaches Beispiel könnten Sie einen Pinsel mit Volltonfarbe innerhalb des CreateDeviceResources-Handlers erstellen und ihn folgendermaßen einer Membervariable zuweisen:

SolidColorBrush m_brush;
m_brush = m_target.CreateSolidColorBrush(Color(1.0f, 0.0f, 0.0f));

In der Draw-Methode für das Fenster lege ich zunächst einen weißen Fensterhintergrund an:

m_target.Clear(Color(1.0f, 1.0f, 1.0f));

Dann zeichne ich mit dem Pinsel ein einfaches rotes Rechteck:

RectF rect (100.0f, 100.0f, 200.0f, 200.0f);
m_target.DrawRectangle(rect, m_brush);

Damit die App nach einem Geräteverlust erfolgreich wiederhergestellt werden kann, muss ich sicherstellen, dass sie den Pinsel genau zum richtigen Zeitpunkt freigibt:

void ReleaseDeviceResources()
{
  m_brush.Reset();
}

Und so leicht ist das Rendern einer CoreWindow-basierten App mit DirectX. Wenn Sie diesen Vorgang mit meinem Artikel von Mai 2013 vergleichen, werden Sie freudig überrascht feststellen, wie viel einfacher die Arbeit mit DirectX-bezogenem Code dank der dx.h-Bibliothek ist. Allerdings gibt es immer noch eine relativ große Anzahl an Codebausteinen, hauptsächlich in Bezug auf die Implementierung der COM-Schnittstellen. Hier kommt C++/CX ins Spiel, um die Verwendung von WinRT-APIs in Ihren Apps zu vereinfachen. Dabei werden bestimmte COM-Codebausteine ausgeblendet, die ich Ihnen in meinen beiden letzten Beiträgen gezeigt habe.

Kenny Kerr ist ein Programmierer aus Kanada, Autor bei Pluralsight und ein Microsoft MVP. Er veröffentlicht Blogs unter kennykerr.ca, und Sie können ihm auf Twitter unter twitter.com/kennykerr folgen.