Condividi tramite


Il presente articolo è stato tradotto automaticamente.

Windows con C++

Rendering per il Runtime di Windows

Kenny Kerr

Kenny KerrNel mio ultimo articolo, esaminato il modello di applicazione Windows Runtime (WinRT) (msdn.microsoft.com/magazine/dn342867). Vi ho mostrato come scrivere un app Store di Windows o Windows Phone con standard di C++ e COM classico, utilizzando solo una manciata di funzioni API WinRT. Non è certamente obbligatorio che è necessario utilizzare una proiezione di lingua come C + + CX o c#. Riuscire a passo intorno a queste astrazioni è una potente funzionalità ed è un ottimo modo per capire come funziona questa tecnologia.

Mia colonna maggio 2013 introdotto Direct2D 1.1 e mostrato come utilizzarlo per eseguire il rendering in un'applicazione desktop (msdn.microsoft.com/magazine/dn198239). La colonna successiva introdotta la libreria dx.h — disponibile da dx.codeplex.com— che semplifica drasticamente DirectX programmazione in C++ (msdn.microsoft.com/magazine/dn201741).

Il codice nel mio ultimo articolo era abbastanza per portare l'applicazione CoreWindow alla vita, ma non fornisce alcun rendering.

Questo mese, vi mostrerò come prendere questo scheletro di base e aggiungere il supporto per il rendering. Il modello di applicazione di WinRT è ottimizzato per il rendering con DirectX. Ti mostrerò come prendere quello che hai imparato nei miei articoli precedenti sul rendering Direct2D e Direct3D e applicarlo all'app basata su CoreWindow WinRT — in particolare utilizzando Direct2D 1.1, tramite la libreria dx.h. Per la maggior parte, l'effettivo Direct2D e Direct3D comandi di disegno che dovrai scrivere sono gli stessi indipendentemente dal fatto se stai mirando il desktop o il Runtime di Windows. Tuttavia, esistono alcune differenze minori, e in primo luogo certamente ottenerlo tutto collegato è molto diversa. Così potrai riprendere da dove lasciato l'ultima volta e mostrarvi come ottenere alcuni pixel sullo schermo!

Al fine di supportare il rendering correttamente, la finestra deve essere consapevole di certi eventi. Come minimo, questi includono modifiche alla visibilità e alla dimensione della finestra, così come le modifiche alla configurazione del DPI logico visualizzazione selezionata dall'utente. Come con l'evento Activated coperto l'ultima volta, questi nuovi eventi sono tutti segnalati all'applicazione tramite callback di interfaccia COM. L'interfaccia di ICoreWindow fornisce i metodi di registrazione per gli eventi VisibilityChanged e SizeChanged, ma prima ho bisogno di attuare i rispettivi gestori. Le due interfacce COM, che ho bisogno di implementare sono molto come il gestore di evento attivato con i suoi modelli classe generata MIDL Microsoft Interface Definition Language:

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

L'interfaccia COM prossimo che devo implementare è chiamato IDisplayPropertiesEventHandler, e per fortuna, è già definito. Semplicemente è necessario includere il file di intestazione rilevanti:

#include <Windows.Graphics.Display.h>

Inoltre, sono definiti i pertinenti tipi dello spazio dei nomi seguente:

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

Date queste definizioni, posso aggiornare la classe SampleWindow dal mio ultimo articolo per ereditare da questi tre interfacce pure:

struct SampleWindow :
  ...
IVisibilityChangedEventHandler,
  IWindowSizeChangedEventHandler,
  IDisplayPropertiesEventHandler

Devo anche ricordarmi di aggiornare la mia implementazione di QueryInterface per indicare il supporto per queste interfacce. Lascio che da fare per voi. Naturalmente, come ho detto l'ultima volta, il Runtime di Windows non si preoccupa dove vengono implementati questi callback di interfaccia COM. Ne consegue che il Runtime di Windows non si assume che la IFrameworkView di mia app, l'interfaccia primaria implementata dalla classe SampleWindow, implementa anche queste interfacce di callback. Così mentre è corretto che QueryInterface gestisce correttamente le query per queste interfacce, il Runtime di Windows non è andando a query per loro. Invece, ho bisogno di registrare per i rispettivi eventi, e il posto migliore per farlo è nell'implementazione del metodo IFrameworkView Load. Come promemoria, il carico è dove si dovrebbe attaccare qualsiasi codice per preparare la tua app per presentazione iniziale. Quindi posso registrarmi per gli eventi VisibilityChanged e SizeChanged all'interno del metodo Load:

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

Questo dice il Windows Runtime esplicitamente dove trovare le implementazioni dell'interfaccia primi due. La terza ed ultima interfaccia è per l'evento LogicalDpiChanged, ma questa registrazione evento è fornita dall'interfaccia IDisplayPropertiesStatics. Questa interfaccia statica viene implementata dalla classe WinRT DisplayProperties. Posso semplicemente utilizzare il modello di funzione GetActivationFactory per ottenere una sospensione di esso (l'implementazione di GetActivationFactory può essere trovato nel mio articolo più recente):

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

La variabile membro detiene questo puntatore a interfaccia, come avrò bisogno di chiamarlo in vari punti durante il ciclo di vita della finestra. Per ora, posso solo registrarsi per l'evento LogicalDpiChanged all'interno del metodo Load:

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

Tornerò per la realizzazione di questi tre interfacce in un attimo. Ora è il momento di preparare l'infrastruttura di DirectX. Servira ' il set standard di gestori di risorse del dispositivo che ho discusso più volte nelle precedenti colonne:

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

Il primo è dove posso creare o caricare le risorse che non sono specifiche per il dispositivo di rendering Direct3D sottostante. I prossimi due sono per la creazione di risorse specifiche del dispositivo. È meglio separare le risorse che sono specifiche per la dimensione della finestra da coloro che non sono. Infine, tutte le risorse del dispositivo devono essere rilasciate. Il rimanente infrastruttura DirectX si basa sull'implementazione di questi quattro metodi correttamente basati sulle esigenze specifiche dell'applicazione app. Esso fornisce i punti discreti nell'app per me gestire risorse di rendering e la creazione di efficiente e riciclaggio di tali risorse.

Ora posso portare in dx.h di prendersi cura di tutto il lavoro pesante di DirectX:

#include "dx.h"

E ogni app Direct2D comincia con la fabbrica di Direct2D:

Factory1 m_factory;

È possibile trovare questo spazio dei nomi Direct2D e includo tipicamente come segue:

using namespace KennyKerr;
using namespace KennyKerr::Direct2D;

La libreria di dx.h ha discreti spazi dei nomi per Direct2D, diretto­scrivere, Direct3D, Microsoft DirectX Graphics Infrastructure (DXGI) e così via. La maggior parte delle mie applicazioni utilizza Direct2D pesantemente, quindi questo ha senso per me. È possibile, ovviamente, gestire gli spazi dei nomi in qualunque modo ha senso per l'app.

La variabile membro m_factory rappresenta la factory Direct2D 1.1. Esso viene utilizzato per creare la destinazione di rendering, come pure una varietà di altre risorse indipendenti dal dispositivo come stati necessari. Io creare il factory Direct2D e quindi consentire qualsiasi device independent risorse essere creato come il passo finale nel metodo Load:

m_factory = CreateFactory();
CreateDeviceIndependentResources();

Dopo che il metodo del carico, la classe di WinRT CoreApplication chiama immediatamente il metodo IFrameworkView Run.

L'implementazione del metodo SampleWindow Run dal mio ultimo articolo bloccato semplicemente chiamando il metodo ProcessEvents nel dispatcher CoreWindow. Bloccando in questo modo è adeguato se l'app è solo andando per eseguire il rendering infrequenti basato su vari eventi. Forse si sta implementando un gioco o solo bisogno di qualche animazione ad alta risoluzione per l'app. L'altro estremo è quello di utilizzare un ciclo continuo di animazione, ma forse si desidera qualcosa di un po ' più intelligente. Ho intenzione di implementare qualcosa di un compromesso tra queste due posizioni. In primo luogo, aggiungerò una variabile membro di tenere traccia di se la finestra è visibile. Questo mi permetterà di strozzare il rendering quando la finestra non è fisicamente visibile all'utente:

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

Allora io posso riscrivere il metodo Run come mostrato Figura 1.

Dinamica figura 1 ciclo di Rendering

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

Come prima, il metodo Run recupera il dispatcher CoreWindow. Poi entra in un ciclo infinito, continuamente rendering e i messaggi di finestra (chiamati "eventi" di Windows Runtime) che possono essere nella coda di elaborazione. Se, tuttavia, la finestra non è visibile, si blocca fino a quando arriva un messaggio. Come funziona l'app sapere quando cambia la visibilità della finestra? Ecco che cosa è l'interfaccia IVisibilityChangedEventHandler per. Ora è possibile implementare il relativo metodo Invoke per aggiornare la variabile membro m_visible:

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

L'interfaccia generato da MIDL utilizza un unsigned char come un tipo di dati booleano portatile. È sufficiente ottenere visibilità corrente della finestra utilizzando il puntatore fornito di interfaccia IVisibilityChangedEventArgs e aggiornare di conseguenza la variabile membro. Questo evento viene generato ogni volta che la finestra è nascosta o visualizzata ed è un po' più semplice di questa implementazione per applicazioni desktop, come si prende cura di un numero di scenari tra cui gestione app shutdown e potenza, per non parlare di commutazione di windows.

Successivamente, è necessario implementare il metodo di rendering chiamato dal metodo Run in Figura 1. Questo è dove lo stack di rendering è stato creato su richiesta e quando si verificano i comandi di disegno effettivi. Lo scheletro di base è mostrato Figura 2.

Figura 2 Schema del metodo Render

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

Il metodo Render dovrebbe risultare familiare. Ha la stessa forma di base che ho delineato prima per Direct2D 1.1. Inizia creando la destinazione di rendering come necessario. Questo è immediatamente seguito dai comandi di disegno effettivi inseriti tra le chiamate a BeginDraw ed EndDraw. Perché la destinazione di rendering è un contesto di periferica Direct2D, in realtà sempre il rendering pixel sullo schermo coinvolge presentando la catena di scambio. A proposito, ho bisogno di aggiungere i tipi di dx.h che rappresenta il contesto di periferica 1.1 Direct2D, così come la versione di DirectX 11.1 della catena di scambio. Quest'ultimo si trova nel namespace Dxgi:

DeviceContext m_target;
Dxgi::SwapChain1 m_swapChain;

Il metodo Render conclude chiamando ReleaseDevice se la presentazione non riesce:

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

Questo si prende cura di rilasciare la catena di swap e di destinazione di rendering. Chiama anche ReleaseDeviceResources per consentire eventuali risorse specifiche per dispositivi quali spazzole, bitmap o effetti per essere rilasciato. Questo metodo di ReleaseDevice potrebbe sembrare irrilevante, ma è fondamentale per la gestione affidabile di perdita del dispositivo in un'applicazione DirectX. Senza rilasciare correttamente tutte le risorse del dispositivo — qualsiasi risorsa che è sostenuta dalla GPU — app non riuscirà a riprendersi dalla perdita del dispositivo e verrà a crollare.

Successivamente, ho bisogno di preparare la destinazione di rendering, il bit omesso dal metodo Render in Figura 2. Inizia con la creazione del dispositivo Direct3D (la libreria di dx.h semplifica davvero pochi passaggi successivi pure):

auto device = Direct3D::CreateDevice();

Con la periferica Direct3D in mano, posso girare per la fabbrica di Direct2D per creare il dispositivo Direct2D e il contesto di periferica Direct2D:

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

Successivamente, è necessario creare la catena di scambio della finestra. Innanzitutto potrai recuperare la fabbrica DXGI dalla periferica Direct3D:

auto dxgi = device.GetDxgiFactory();

Quindi posso creare una catena di scambio per CoreWindow dell'applicazione:

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

Qui, ancora una volta, la libreria di dx.h rende la mia vita molto più semplice compilando automaticamente la struttura di DXGI_SWAP_CHAIN_DESC1 per me. Poi chiamerò al metodo CreateDeviceSwapChainBitmap per creare una bitmap di Direct2D che rappresenterà il buffer nascosto della catena di scambio:

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

Questo metodo deve prima a descrivere il buffer nascosto della catena di scambio in modo che abbia senso per Direct2D. BitmapProperties1 è la versione dx.h della struttura D2D1_BITMAP_PROPERTIES1 Direct2D. La costante BitmapOptions::Target indica che la bitmap verrà utilizzata come destinazione di un contesto di periferica. La Bitmap­Options::CannotDraw costante si riferisce al fatto che la catena di scambio di nuovo tampone può essere utilizzato solo come uscita e non come input per altre operazioni di disegno. PixelFormat è la versione dx.h della struttura D2D1_PIXEL_FORMAT Direct2D.

Con la proprietà bitmap definite, il metodo CreateBitmapFromDxgiSurface recupera il buffer nascosto della catena di scambio e crea una bitmap Direct2D per rappresentarlo. In questo modo, il contesto di periferica Direct2D può rendere direttamente alla catena di scambio semplicemente prendendo di mira la bitmap tramite il metodo SetTarget.

Nel metodo Render, basta dire Direct2D come qualsiasi funzione configurazione DPI dell'utente i comandi di disegno in scala:

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

Poi chiamerò ai gestori di risorse dispositivo dell'app per creare eventuali risorse come necessario. Per riassumere, Figura 3 fornisce la sequenza di inizializzazione del dispositivo completo per il metodo Render.

Figura 3 preparando la destinazione di rendering

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

Anche se il ridimensionamento DPI è correttamente applicato immediatamente dopo aver creato il contesto di periferica Direct2D, deve anche essere aggiornato ogni qualvolta questa impostazione viene modificata dall'utente. Il fatto che il ridimensionamento DPI può cambiare per un app in esecuzione è una novità di Windows 8. Questo è dove arriva l'interfaccia IDisplayPropertiesEventHandler. Posso ora semplicemente implementare il relativo metodo Invoke e aggiornare di conseguenza il dispositivo. Qui è il gestore di eventi LogicalDpiChanged:

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

Supponendo che il bersaglio — il contesto di periferica — è stato creato, recupera il valore DPI logico corrente e la inoltra semplicemente a Direct2D. Chiama quindi all'app di ricreare qualsiasi dispositivo-dimensione -­risorse specifiche prima di eseguire nuovamente il rendering. In questo modo, mio app può rispondere dinamicamente ai cambiamenti nella configurazione di DPI del display. L'ultima modifica che la finestra deve gestire dinamicamente è quella di modifiche alle dimensioni della finestra. Io ho già collegato la registrazione dell'evento, quindi devo semplicemente aggiungere l'implementazione del metodo Invoke di IWindowSizeChangedEventHandler, che rappresenta il gestore di evento SizeChanged:

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

L'unica cosa che resta da fare è ridimensionare la bitmap di catena di scambio tramite il metodo ResizeSwapChainBitmap. Ancora una volta, questo è qualcosa che deve essere maneggiato con cautela. Ridimensionamento buffer della catena di scambio può e dovrebbe essere un funzionamento efficiente, ma solo se fatto correttamente. In primo luogo, affinché questa operazione venga eseguita correttamente, ho bisogno di garantire a che tutti i riferimenti a questi tamponi sono stati rilasciati. Questi possono essere riferimenti che l'app è in possesso direttamente così come indirettamente. In questo caso, il riferimento è tenuto dal contesto di periferica Direct2D. L'immagine di destinazione è la bitmap Direct2D che creato per avvolgere il buffer nascosto della catena di scambio. Rilasciando questo è abbastanza facile:

m_target.SetTarget();

Posso quindi chiamare il metodo di ResizeBuffers della catena di scambio per fare tutto il lavoro pesante e quindi chiamare ai gestori di risorse dispositivo dell'app come necessario. Figura 4 vi mostra come questo si riunisce.

Figura 4 la catena di scambio il ridimensionamento

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

È ora possibile aggiungere alcuni comandi di disegno, e potrai essere reso efficiente di DirectX alla destinazione della CoreWindow. Un semplice esempio, si potrebbe voler creare un pennello tinta all'interno del gestore CreateDeviceResources e assegnarlo a una variabile membro come segue:

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

All'interno del metodo Draw della finestra, posso iniziare deselezionando su sfondo bianco della finestra:

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

Quindi posso usare il pennello e disegnare un rettangolo rosso semplice come segue:

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

Per garantire che l'applicazione può recuperare con grazia da perdita del dispositivo, devo assicurarmi che rilascia il pennello al momento giusto:

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

E questo è tutto quello che serve per eseguire il rendering di un'applicazione basata su CoreWindow con DirectX. Naturalmente, se si confronta questo alla mia colonna maggio 2013, dovrebbe piacevolmente sorpresi di come molto più semplice il codice correlato DirectX funziona a essere grazie alla libreria dx.h. Ma così com'è, c'è ancora una buona quantità di codice standard, principalmente legata alla implementazione delle interfacce COM. Questo è dove C + + CX entra e semplifica l'uso di WinRT APIs dentro le app. Esso nasconde alcune delle boilerplate codice COM che vi ho mostrato in queste ultime due colonne.

Kenny Kerr è un programmatore di computer basato nel Canada, un autore per Pluralsight e MVP Microsoft. Ha Blog a kennykerr.ca e si può seguirlo su Twitter a twitter.com/kennykerr.