DirectX

Usando XAML com DirectX e C++ em aplicativos da Windows Store

Doug Erickson

 

Desde o Windows Vista, o DirectX tem sido a API de gráficos principal para a plataforma Windows, permitindo a aceleração da GPU (unidade de processamento de gráficos) para todas as operações de desenho de tela de sistema operacional. No entanto, até o Windows 8, os desenvolvedores do DirectX tinham de implantar suas próprias estruturas de interface de usuário do zero em linguagem C++ e COM nativa ou licenciar um pacote de interface do usuário de middleware, como o Scaleform.

No Windows 8, você pode preencher a lacuna entre o DirectX nativo e uma estrutura de interface de usuário adequada com o recurso de interoperabilidade DirectX-XAML do Tempo de Execução do Windows (WinRT). Para tirar proveito do suporte de API para XAML no DirectX, você precisa usar C++ "nativo" (embora você tenha acesso a ponteiros inteligentes e às extensões do componente C++). Um pouco de conhecimento básico em COM também ajuda, mas vou detalhar a interoperabilidade específica que você deve executar para unir a estrutura XAML e as operações DirectX.

Nesta série de artigos de duas partes, falarei de duas abordagens para a interoperabilidade DirectX-XAML: uma em que desenho superfícies em elementos de estrutura XAML com as APIS de gráficos do DirectX; e outra em que desenho a hierarquia de controles XAML sobre uma superfície de cadeia de troca do DirectX.

Este artigo fala sobre o primeiro cenário, em que você processa imagens ou primitivos exibidos no sua estrutura XAML.

Mas, primeiro, aqui está uma rápida visão geral de suas opções de API. Atualmente, existem três tipos de XAML no Tempo de Execução do Windows que oferecem suporte à interoperabilidade DirectX:

  • Windows::UI::Xaml::Media::Imaging::SurfaceImage­Source (SurfaceImageSource hereafter): esse tipo permite desenhar conteúdo relativamente estático em uma superfície XAML compartilhada usando APIs de gráficos do DirectX. A exibição é totalmente gerenciada pela estrutura WinRT XAML, o que significa que todos os elementos de apresentação são gerenciados da mesma forma por ela. Isso a torna ideal para desenhar conteúdo complexo que não muda a cada quadro, mas menos ideal para jogos 2D ou 3D complexos que são atualizados com alta frequência.
  • Windows::UI::Xaml::Media::Imaging::VirtualSurface­ImageSource (VirtualSurfaceImageSource hereafter): como o SurfaceImageSource, ele usa os recursos gráficos definidos para a estrutura XAML. Diferente do SurfaceImage­Source, o VirtualSurfaceImageSource oferece suporte a superfícies logicamente grandes de forma otimizada, baseada em região, de forma que o DirectX desenha apenas as regiões da superfície que mudam entre as atualizações. Escolha esse elemento se estiver criando um controle de mapa como, por exemplo, um visualizador de documentos grandes com muitas imagens. Novamente, como o SurfaceImageSource, esse tipo não é uma boa opção para jogos 2D ou 3D complexos, especialmente aqueles que dependem de feedback e visual em tempo real.
  • Windows::UI::Xaml::Controls::SwapChainBackgroundPanel (SwapChainBackgroundPanel hereafter): esse tipo e elemento de controle XAML permite que seu aplicativo use um provedor de exibição personalizado do DirectX (e corrente de troca) sobre o qual você pode desenhar elementos XAML, e que proporciona um desempenho melhor em situações que exigem apresentação de latência muito baixa ou feedback com alta frequência (por exemplo, jogos modernos). Seu aplicativo irá gerenciar o contexto de dispositivo DirectX para o SwapChainBackgroundPanel separadamente da estrutura XAML. Naturalmente, isso significa que o SwapChainBackgroundPanel e os quadros XAML não são sincronizados entre si para atualização. Você também pode processar um SwapChainBackgroundPanel de um thread de segundo plano.

Desta vez, vou falar das APIs SurfaceImageSource e Virtual­SurfaceImageSource, e como você pode incorporá-las em seus controles XAML cheios de imagens e mídia (SwapChainBackgroundPanel é especial e tem seu próprio artigo).

Observação: SurfaceImageSource e VirtualSurfaceImageSource podem ser usados em linguagem C# ou Visual Basic .NET, embora o componente de renderização DirectX deva ser escrito em C++ e compilado em uma DLL separada, acessada por meio do projeto C#. Também existem estruturas WinRT DirectX gerenciadas por terceiros, como SharpDX (sharpdx.org) e MonoGame (monogame.net), que você pode usar em vez do SurfaceImageSource ou VirtualSurfaceImageSource.

Vamos começar. Este artigo presume que você conheça os fundamentos do DirectX, especificamente Direct2D, Direct3D e DXGI (Microsoft DirectX Graphics Infrastructure). Naturalmente você conhece XAML e C++; esse é um assunto intermediário para desenvolvedores de aplicativos do Windows. Com isso dominado: avante!

SurfaceImageSource e composição de imagem no DirectX

O namespace Windows::UI::Xaml::Media::Imaging contém o tipo SurfaceImageSource, juntamente com muitos outros tipos de imagens XAML. Na verdade, o tipo SurfaceImageSource fornece uma maneira de desenhar dinamicamente nas superfícies compartilhadas de muitos primitivos de gráficos e imagens XAML, preenchendo-os efetivamente com o conteúdo que você processa com chamadas de gráficos DirectX e aplicando-os como pincel. (Especificamente, é um ImageSource que você usa como ImageBrush.) Pense nisso como um bitmap que você está gerando em tempo real com o DirectX e considere que é possível usar esse tipo em muitos locais em que você pode aplicar um bitmap ou outro recurso de imagem.

Para a finalidade desta seção, vou desenhar em um elemento XAML <Image>que contém uma imagem PNG em branco como espaço reservado. Especifico uma altura e largura do elemento <Image>, pois essa informação é transmitida ao construtor de SurfaceImageSource em meu código (se eu não fornecer uma altura e largura, o conteúdo que eu renderizar será esticado para se ajustar aos parâmetros da marca <Image>):

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

Neste exemplo, meu alvo é a marca <Image>, que exibirá a superfície em que estou desenhando. Eu poderia usar um primitivo XAML também, como <Rectangle>ou <Ellipse>, que podem ser preenchidos por um pincel SurfaceImageSource. Isso é possível porque os desenhos desses primitivos e imagens são realizados com DirectX pelo Tempo de Execução do Windows; tudo que estou fazendo é camuflar uma fonte de renderização diferente por debaixo dos panos, por assim dizer.

Em meu código, incluo o seguinte:

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

Estes são os cabeçalhos para o Windows Runtime Library (WRL), alguns componentes chaves do DirectX e, mais importante, a interoperabilidade de DirectX nativa interfaces. A necessidade deste último ser incluído se tornará aparente logo.

Também importo as bibliotecas correspondentes: dxgi.lib, d2d1.lib e d3d11.lib.

E para sua conveniência, também vou incluir os seguintes namespaces:

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

Agora, no código, criar um tipo, MyImageSourceType, que herda do tipo base SurfaceImageSource e chama seu construtor, como mostrado na Figura 1.

Figura 1 Derivação 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();
  }
  // ...
}

Observação: você não precisa herdar de SurfaceImageSource, embora isso torne as coisas um pouco mais fáceis sob a perspectiva de organização do código. Você pode simplesmente instanciar um objeto SurfaceImageSource como membro e usá-lo em vez disso. Mentalmente, apenas substitua o nome de seu membro pela autorreferência do objeto (esta) nos exemplos de código.

Os métodos CreateDeviceResources e CreateDeviceIndependent­Resources são implementações de usuário que são uma maneira conveniente de separar logicamente a configuração específica para a interface de hardware gráfica do DirectX e a configuração específica de aplicativo mais geral do DirectX. As ações executadas em ambos os métodos são essenciais. No entanto, é bom (e necessário) para separá-las no design, pois há momentos em que você pode querer recriar os recursos do dispositivo sem afetar os recursos independentes do dispositivo, e vice-versa.

CreateDeviceResources deve ser parecido com o código na Figura 2, pelo menos de forma básica.

Figura 2 Criando os recursos específicos do dispositivo 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());
}

Até agora, criei um contexto de dispositivo de hardware e o vinculei a um... espere, o que é ISurfaceImageSourceNative? Não é um tipo WinRT! O que está acontecendo aqui?

É um bit de interoperabilidade. É aí que eu dou uma olhada no "tubo Jeffries" do WRL e modifico um pouco as ligações. Também é onde entro no COM, que fica atrás de grande parte do WRL.

Para habilitar esse comportamento de interoperabilidade, preciso essencialmente ligar essa fonte de DirectX coberta. Para fazer isso, preciso incluir meu tipo na implementação dos métodos definidos na interface COM específica do WRL, ISurfaceImageSourceNative. Quando eu fizer isso, vou anexar o tipo ao elemento <Image>(neste exemplo) e, quando o aplicativo enviar uma atualização à estrutura XAML, usarei minhas implementações DirectX das chamadas de desenho em vez das implementações padrão.

ISurfaceImageSourceNative é definido no cabeçalho da interoperabilidade que especifiquei anteriormente. Vê o que está acontecendo aqui?

Agora, em meu método CreateDeviceIndependentResources específico do aplicativo, separei o COM e a consulta dos métodos nativos definidos em SurfaceImageSource. Como esses métodos não são expostos diretamente, eles devem ser obtidos com uma chamada para IUnknown::Query­Interface no tipo derivado de SurfaceImageSource ou SurfaceImageSource. Para fazer isso, eu reformulo meu tipo derivado de SurfaceImageSource como IUnknown, a interface base para qualquer interface COM (também poderia formular como IInspectable, a interface "base" para qualquer tipo do WinRT, que herda de IUnknown). Então, para obter uma lista dos métodos ISurfaceImageSourceNative, consulto essa interface da seguinte forma:

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

(IID_PPV_ARGS é uma macro auxiliar para o WRL que recupera um ponteiro de interface. Muito conveniente! Se você não estiver herdando de SurfaceImageSource, substitua o nome do membro do objeto SurfaceImageSource para isso.)

Finalmente, esta parte do método CreateDeviceResources faz sentido:

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

ISurfaceImageSourceNative::SetDevice assume a interface gráfica configurada e a une à superfície para qualquer operação de desenho. Observe, porém, que isso também significa que eu devo chamar Create­DeviceResources depois de chamar CreateDeviceIndependentResources pelo menos uma vez antes – ou eu não terei um dispositivo configurado para conectar.

Agora expus a implementação subjacente de ISurfaceImageSourceNative do tipo SurfaceImageSource do qual meu tipo MyImageSourceType deriva. Eu efetivamente abri a tampa e mudei os fios para o tipo SurfaceImageSource, ainda que para a implementação base das chamadas de desenho e não minhas próprias. Agora, vou implementar minhas chamadas.

Para isso, implemento os seguintes métodos:

  • BeginDraw: abre o contexto de dispositivo para o desenho.
  • EndDraw: fecha o contexto de dispositivo.

Observação: escolhi os nomes de métodos BeginDraw e EndDraw para uma correspondência meio solta com os métodos ISurfaceImageSourceNative. Esse padrão é para conveniência e não é imposto.

Meu método BeginDraw (ou outro método de inicialização de desenho que defino no tipo derivado) deve, em algum momento, chamar ISurface­ImageSourceNative::BeginDraw. (Para otimizações, você pode adicionar um parâmetro para um sub-retângulo com a região da imagem a ser atualizada.) Da mesma forma, o método EndDraw deve chamar ISurface­ImageSourceNative::EndDraw.

Os métodos BeginDraw e EndDraw, neste caso, poderiam ser algo como o código mostrado na Figura 3.

Figura 3 Desenho na superfície do 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();
}

Note que meu método BeginDraw assume um primitivo Rect como entrada, que é mapeado para um tipo RECT nativo. Esse RECT define a região da tela em que pretendo desenhar com um SurfaceImageSource correspondente. BeginDraw, no entanto, pode ser chamada somente uma por vez; terei de enfileirar as chamadas BeginDraw para cada SurfaceImageSource, uma após a outra.

Note também que eu inicializo uma referência a uma IDXGISurface e uma estrutura de PONTO de deslocamento que contém coordenadas de deslocamento (x, y) do RECT que desenharei na IDXGISurface em relação ao canto superior esquerdo. Esse ponteiro de superfície e deslocamento são retornados de ISurfaceImageSourceNative::BeginDraw para fornecer a IDXGISurface para o desenho. As chamadas futuras no exemplo criam um bitmap do ponteiro de superfície recebido e desenham nele com chamadas Direct2D. Quando ISurfaceImageSourceNative::EndDraw é chamado na sobrecarga de EndDraw, uma imagem completa é o resultado final — uma imagem que estará disponível para desenho no elemento de imagem ou primitivo XAML.

Vejamos o que eu tenho:

  • Um tipo que derivei de SurfaceImageSource.
  • Métodos no meu tipo derivado que definem seu comportamento de desenho para um RECT fornecido na tela.
  • Os recursos gráficos do DirectX de que preciso para executar o desenho.
  • Uma associação entre o dispositivo gráfico DirectX e o SurfaceImageSource.

O que eu ainda preciso é:

  • Algum código que renderize imagem real em RECT.
  • Uma conexão entre a instância específica de <Image> (ou primitivo) no XAML e a instância de SurfaceImageSource, a ser invocada pelo aplicativo.

O código para o comportamento de desenho depende de mim, e é provavelmente mais simples implementá-lo em meu tipo SurfaceImageSource como um método público específico que pode ser chamado do code-behind.

O resto é fácil. No code-behind do meu XAML, adiciono este código ao meu construtor:

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

E adiciono um manipulador de eventos no mesmo code-behind, semelhante a este:

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

(Opcionalmente, se eu não estivesse derivando de SurfaceImageSource, eu poderia colocar as chamadas para BeginDraw e EndDraw em um método — como DrawTheMostAmazingSpinning3DShadedCubeEver do trecho de código anterior — no meu objeto de desenho.)

Agora, se eu estiver usando um primitivo XAML, como Rect ou Ellipse, crio um ImageBrush e anexo o SurfaceImageSource a ele, como este (onde MySISPrimitive é um primitivo de gráfico 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;

E está pronto! Para recapitular, o processo em meu exemplo é:

  1. Escolho um elemento de imagem XAML, como Image, ImageBrush ou primitivos de gráficos (Rect, Ellipse etc.), em que pretendo renderizar. Determino também se a superfície fornecerá uma imagem animada. Coloco em meu XAML.
  2. Crio o dispositivo DirectX e os contextos de dispositivo (normalmente Direct2D ou Direct3D, ou ambos) que serão usados nas operações de desenho. Além disso, usando o COM, adquiro uma referência para a interface ISurfaceImageSourceNative que serve de base para o tipo de tempo de execução de SurfaceImageSource e associo o dispositivo gráfico a ela.
  3. Crio um tipo que deriva de SurfaceImageSource, e que tem o código que chama ISurfaceImageSource::BeginDraw e ISurfaceImageSource::EndDraw.
  4. Adiciono as operações específicas de desenho como métodos no tipo SurfaceImageSource.
  5. Para as superfícies Image, conecto a propriedade Source a uma instância do tipo SurfaceImageSource. Para superfícies de primitivos gráficos, crio um ImageBrush e atribuo uma instância de SurfaceImage­Source à propriedade de ImageSource e depois uso esse pincel com a propriedade Fill do primitivo (ou qualquer propriedade que aceite ImageSource ou ImageBrush).
  6. Chamo as operações de desenho nas instâncias de SurfaceImageSource a partir de manipuladores de eventos. Para imagens animadas, é preciso garantir que as operações de desenho em quadro possam ser interrompidas.

Posso usar Surface­ImageSource para cenários de jogos 2D e 3D se a cena e os sombreadores forem simples o bastante. Por exemplo, um jogo de estratégia graficamente mediano (como o "Civilization 4") ou um simples rastreador de masmorra poderia render os visuais para um SurfaceImageSource.

Além disso, observe que posso criar o tipo derivado SurfaceImageSource em C++ em uma DLL separada e usar esse tipo em uma projeção de linguagem diferente, não C++. Nesse caso, eu poderia limitar meu renderizador e métodos ao C++ e construir minha infraestrutura de aplicativo e code-behinds em, digamos, C#. Regras Model-View-ViewModel (MVVM)!

Que nos traz as limitações:

  • O controle que exibe o SurfaceImageSource destina-se a superfícies de tamanho fixo.
  • O controle que exibe o SurfaceImageSource não tem otimizado desempenho para superfícies arbitrariamente grandes, especialmente as superfícies que podem ser dinamicamente estendidas ou ampliadas.
  • A atualização do controle é tratada pelo provedor de exibição de estrutura WinRT XAML, que ocorre quando a estrutura é atualizada. Para cenários de gráficos em tempo real de alta fidelidade, isso pode afetar visivelmente o desempenho (que significa que não é adequado para seu jogo de batalha intergaláctica cheio dos sombreadores novos que são a sensação do momento).

Isso nos leva ao VirtualSurfaceImageSource (e, por fim, ao SwapChainBackgroundPanel). Vamos dar uma olhada no primeiro.

VirtualSurfaceImageSource e renderização de controle interativo

VirtualSurfaceImageSource é uma extensão do SurfaceImageSource, mas é projetado para superfícies de imagem que podem ser redimensionadas pelo usuário, especialmente imagens que podem ser dimensionadas para um tamanho maior do que o da tela ou movidas parcialmente para fora da tela, ou que podem ter imagens ou elementos XAML que obscurecem parte de ou todas elas. Funciona particularmente bem com aplicativos em que o usuário regularmente estende ou amplia uma imagem potencialmente maior que a tela, por exemplo, um controle de mapa ou um visualizador de imagens.

O processo do VirtualSurfaceImageSource é idêntico ao do SurfaceImageSource, conforme apresentado anteriormente, só que você usa o tipo VirtualSurfaceImageSource em vez de SurfaceImageSource, e a implementação da interface IVirtualImageSourceNative em vez da ISurfaceImageSourceNative.

Isso significa que eu mudo meu código do exemplo anterior para:

  • Uso VirtualSurfaceImageSource em vez de SurfaceImage­Source. Nos exemplos de código a seguir, terei minha classe de tipo de fonte de imagem base, MyImageSourceType, derivada de VirtualSurfaceImageSource.
  • Consulto a implementação do método na interface IVirtualSurfaceImageSourceNative subjacente.

Consulte a Figura 4 para obter um exemplo.

Figura 4 Herança 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));
  }
  // ...
}

Ah, e há outra diferença muito importante: Devo implementar um retorno de chamada que é invocado sempre que um "bloco" (uma região retangular definida, não deve ser confundido com os blocos da interface de usuário do Windows 8) da superfície torna-se visível e deve ser desenhado. Esses blocos são gerenciados pela estrutura quando um aplicativo cria uma instância de VirtualSurfaceImageSource, e você não controla seus parâmetros. Em vez disso, nos bastidores, uma imagem grande é subdividida nesses blocos, e o retorno de chamada é invocado sempre que uma parte de um desses blocos se torna visível para o usuário e requer uma atualização.

Para usar esse mecanismo, preciso implementar primeiro um tipo instanciável que herda da interface IVirtualSurfaceUpdatesCallbackNative e registrar uma instância desse tipo, passando-a para IVirtualSurfaceImageSource::RegisterForUpdatesNeeded, conforme mostrado na Figura 5.

Figura 5 Configuração de um retorno de chamada para 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);
  // ...
}

A operação de desenho é implementada como o método UpdatesNeeded da interface IVirtualSurfaceUpdatesCallbackNative. Se uma região específica ficar visível, devo determinar quais blocos devem ser atualizados. Faço isso chamando IVirtualSurfaceImage­SourceNative::GetRectCount chamada e, se o número de blocos atualizados for maior que zero, obtendo os retângulos específicos para os blocos atualizados com IVirtualSurfaceImageSourceNative::GetUpdateRects e atualizando cada um:

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

Posso obter os parâmetros definidos por VirtualSurfaceImageSource para esses blocos como objetos RECT. No exemplo anterior, posso obter uma matriz de objetos RECT para todos os blocos que precisam de atualizações. Em seguida, uso os valores dos RECTs para redesenhar os blocos, fornecendo-os a VirtualSurfaceImageSource::BeginDraw.

Novamente, como no caso de SurfaceImageSource, inicializo um ponteiro na IDXGISurface e chamo o método BeginDraw em IVirtualSurfaceImageSourceNative (a implementação de interface nativa subjacente) para obter a superfície atual na qual desenhar. O deslocamento, no entanto, refere-se ao deslocamento (x, y) do RECT de destino, e não ao elemento de imagem como um todo.

Para cada RECT a ser atualizado, chamo um código semelhante ao da Figura 6.

Figura 6 Administrando atualizações para o tamanho ou a visibilidade do controle

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

Novamente, não posso paralelizar essas chamadas, pois a interface gráfica pode ter apenas uma operação no thread da interface de usuário por vez. Posso processar cada bloco RECT em série, ou posso chamar IVirtualSurface­ImageSourceNative::BeginDraw com uma área unida de todos os RECTs para uma única atualização de desenho. Isso fica a cargo do desenvolvedor.

Finalmente, chamo IVirtualSurfaceImageSourceNative::EndDraw depois de atualizar cada bloco RECT atualizado. Quando o último bloco for processado, terei um bitmap completamente atualizado para fornecer à imagem ou ao primitivo XAML correspondente, tal como fiz no exemplo SurfaceImageSource.

E está pronto! Essa forma de interoperabilidade DirectX-XAML é ótima quando os usuários não se importam com a entrada de baixa latência para gráficos 3D em tempo real tanto quanto em um jogo detalhado e em tempo real. Também é impressionante para controles e aplicativos ricos em gráficos e jogos mais assíncronos (em que os jogadores aguardam sua vez de jogar).

No artigo seguinte, falarei sobre o outro lado dessa abordagem: desenhar XAML sobre uma cadeia de troca do DirectX e o trabalho necessário para a estrutura XAML fazer bonito com um provedor de exibição personalizado do DirectX. Fique sintonizado!

Get help building your Windows Store app!

Doug Erickson é escritor sênior de programação da Microsoft, que trabalha nos Serviços de Conteúdo do Windows e é especializado em desenvolvimento de jogos para DirectX e Windows Store. Ele espera que você crie muitos jogos surpreendentes para Windows Store e DirectX e fique famoso por isso.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Jesse Bishop e Bede Jordan