Windows com C++

Uma biblioteca C++ moderna para programação no DirectX

Kenny Kerr

Baixar o código de exemplo

Kenny Kerr
Já escrevi muitos códigos DirectX e também vários textos sobre o DirectX. Até mesmo produzi cursos de treinamento online para o DirectX. Na verdade, não é tão difícil como alguns desenvolvedores fazem parecer. Definitivamente há uma curva de aprendizado, mas assim que você supera essa parte, não é difícil entender como e por que o DirectX funciona. Ainda assim, admito que a família DirectX de APIs poderia ser mais fácil de usar.

Algumas noites atrás, decidi remediar essa situação. Fiquei acordado durante a noite inteira e escrevi um pequeno arquivo de cabeçalho. Algumas noites mais tarde, o arquivo tinha quase 5.000 linhas de código. Meu objetivo era oferecer algo que pudesse ser usado para criar aplicativos mais facilmente com o Direct2D e desafiar todos os argumentos de que “C++ é difícil” ou “DirectX é difícil” que são tão comuns atualmente. Não queria produzir mais um wrapper pesado para o DirectX. Em vez disso, decidi aproveitar o C++11 para produzir uma API mais simples para o DirectX sem gerar sobrecarga de espaço e tempo na API principal do DirectX. Você pode encontrar essa biblioteca que desenvolvi em dx.codeplex.com.

A biblioteca consiste inteiramente em um único arquivo de cabeçalho chamado dx.h — o restante do código-fonte no CodePlex fornece exemplos de uso.

Nesta coluna, mostrarei como é possível usar a biblioteca para realizar mais facilmente várias atividades comuns relacionadas ao DirectX. Também descreverei o design dessa biblioteca para que você tenha uma ideia de como o C++11 pode ajudar a tornar mais agradáveis as APIs de COM clássicas sem recorrer a wrappers de alto impacto, como o Microsoft .NET Framework.

Obviamente, o foco está no Direct2D. Essa continua sendo a maneira mais simples e eficiente de utilizar o DirectX para uma ampla classe de aplicativos e jogos. Muitos desenvolvedores parecem se dividir em dois grupos opostos. Existem os desenvolvedores especializados no DirectX que trabalharam desde o início em várias versões da API do DirectX. Eles se desenvolveram após anos de evolução do DirectX e se sentem contentes por fazerem parte de um clube exclusivo e bastante exigente do qual poucos desenvolvedores podem participar. No outro grupo, estão aqueles desenvolvedores que ouviram o boato de que o DirectX é difícil e que preferem manter distância dele. Eles também tendem a rejeitar o C++ sem pensar duas vezes.

Não faço parte de nenhum desses grupos. Acredito que o C++ e o DirectX não precisam ser difíceis. Na coluna do mês passado (msdn.microsoft.com/magazine/dn198239), apresentei o Direct2D 1.1 e o código obrigatório do Direct3D e do DirectX Graphics Infrastructure (DXGI) para criar um dispositivo e gerenciar uma cadeia de troca. O código para criar um dispositivo Direct3D com a função D3D11CreateDevice adequada para renderização de GPU ou CPU tem aproximadamente 35 linhas. No entanto, com a ajuda de meu pequeno arquivo de cabeçalho, resume-se a isto:

 

auto device = CreateDevice();

A função CreateDevice retorna um objeto Device1. Todas as definições do Direct3D estão no namespace do Direct3D, portanto, posso ser mais explícito e escrever da seguinte maneira:

Direct3D::Device1 device = Direct3D::CreateDevice();

O objeto Device1 é simplesmente um wrapper em torno de um ponteiro de interface COM ID3D11­Device1, a interface de dispositivo Direct3D apresentada com o lançamento do DirectX 11.1. A classe Device1 deriva da classe Device que, por sua vez, é um wrapper em torno da interface original ID3D11Device. Representa uma referência e não gera sobrecarga adicional em comparação com apenas manter o próprio ponteiro de interface. Observe que Device1 e sua classe pai, Device, são classes regulares C++, e não interfaces. Você poderia considerá-las ponteiros inteligentes, mas isso seria muito simplista. É claro que elas lidam com contagem de referência e fornecem o operador “->” para chamar diretamente o método de sua escolha, mas começam a se destacar de verdade quando você inicia o uso dos vários métodos não virtuais fornecidos pela biblioteca dx.h.

Eis aqui um exemplo. Com frequência, talvez você precise da interface DXGI do dispositivo Direct3D para passar para algum outro método ou função. Você pode usar o método mais difícil:

auto device = Direct3D::CreateDevice();
wrl::ComPtr<IDXGIDevice2> dxdevice;
HR(device->QueryInterface(dxdevice.GetAddressOf()));

Com certeza funciona, mas agora você também precisa lidar diretamente com a interface de dispositivo DXGI. Você também precisa se lembrar de que a interface IDXGIDevice2 é a versão da interface de dispositivo DXGI do DirectX 11.1. Em vez disso, você pode simplesmente chamar o método AsDxgi:

auto device = Direct3D::CreateDevice();
auto dxdevice = device.AsDxgi();

O objeto Device2 resultante, definido no namespace Dxgi desta vez, encapsula o ponteiro de interface COM IDXGIDevice2, fornecendo seu próprio conjunto de métodos não virtuais. Como outro exemplo, talvez você queira usar o “modelo de objeto” do DirectX para chegar ao alocador do DXGI:

auto device   = Direct3D::CreateDevice();
auto dxdevice = device.AsDxgi();
auto adapter  = dxdevice.GetAdapter();
auto factory  = adapter.GetParent();

Como esse é um padrão muito comum, é claro que a classe Device do Direct3D fornece o método GetDxgiFactory como um atalho conveniente:

auto d3device = Direct3D::CreateDevice();
auto dxfactory = d3device.GetDxgiFactory();

Portanto, exceto por alguns métodos e funções de conveniência, como o GetDxgiFactory, os métodos não virtuais são mapeados individualmente para os métodos e funções subjacentes de interface do DirectX. Isso pode não parecer muito, mas uma série de técnicas são combinadas para produzir um modelo de programação muito mais conveniente e produtivo para o DirectX. A primeira é o uso de enumerações com escopo. A família DirectX de APIs define uma matriz espantosa de constantes, sendo que muitas delas são enums, sinalizadores ou constantes tradicionais. Elas não são fortemente tipadas, são difíceis de encontrar e não funcionam bem com o Visual Studio IntelliSense. Ignorando as opções do alocador por um minuto, eis o que você precisa para criar um alocador do Direct2D:

wrl::ComPtr<ID2D1Factory1> factory;
 HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
factory.GetAddressOf()));

O primeiro parâmetro da função D2D1CreateFactory é uma enum, mas como não é uma enum com escopo, é difícil encontrá-la com o Visual Studio IntelliSense. Essas enums com estilo antigo fornecem um pouco de segurança de tipos, mas não muita. Na melhor das hipóteses, um código de resultado E_INVALIDARG será mostrado em tempo de execução. Não sei o que você acha disso, mas prefiro identificar esses erros em tempo de compilação ou, melhor ainda, evitá-los completamente.

auto factory = CreateFactory(FactoryType::MultiThreaded);

Mais uma vez, não preciso perder tempo pesquisando para descobrir qual é a versão mais recente da interface de alocador do Direct2D que é chamada. Certamente, o maior benefício aqui é a produtividade. É claro que a API do DirectX não se resume apenas a criar e chamar métodos da interface COM. Muitas estruturas de dados básicas são usadas para reunir várias propriedades e parâmetros. A descrição de uma cadeia de troca é um bom exemplo. Com todos seus membros confusos, nunca consigo me lembrar exatamente como essa estrutura deve ser preparada, sem mencionar as especificidades da plataforma. Aqui, mais uma vez, a biblioteca é útil, ao oferecer um substituto para a intimidadora estrutura DXGI_SWAP_CHAIN_DESC1:

SwapChainDescription1 description;

Nesse caso, substitutos compatíveis com o binário são fornecidos para garantir que o DirectX veja o mesmo tipo, mas você possa usar algo um pouco mais prático. Não é diferente do que o Microsoft .NET Framework fornece com seus wrappers P/Invoke. O construtor padrão fornece padrões adequados para a maioria dos aplicativos da Windows Store e da área de trabalho. Talvez você queira, por exemplo, fazer a substituição para aplicativos da área de trabalho para produzir uma renderização mais suave durante o redimensionamento:

SwapChainDescription1 description;
description.SwapEffect = SwapEffect::Discard;

A propósito, esse efeito de troca também é necessário para o Windows Phone 8, mas não é permitido em aplicativos da Windows Store. Vai entender.

Muitas das melhores bibliotecas conseguem encaminhá-lo de maneira rápida e fácil para uma solução que funciona. Vamos considerar um exemplo concreto. O Direct2D fornece um pincel de gradiente linear. A criação desse tipo de pincel envolve três etapas lógicas: definir as marcas de gradiente, criar a coleção de marcas de gradiente e, em seguida, criar o pincel de gradiente linear considerando essa coleção. A Figura 1 mostra como seria a aparência disso usando a API do Direct2D diretamente.

Figura 1 Criando um pincel de gradiente linear da maneira mais difícil

D2D1_GRADIENT_STOP stops[] =
{
  { 0.0f, COLOR_WHITE },
  { 1.0f, COLOR_BLUE },
};
wrl::ComPtr<ID2D1GradientStopCollection> collection;
HR(target->CreateGradientStopCollection(stops,
   _countof(stops),
   collection.GetAddressOf()));
wrl::ComPtr<ID2D1LinearGradientBrush> brush;
HR(target->CreateLinearGradientBrush(D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES(),
   collection.Get(),
   brush.GetAddressOf()));

Com a ajuda do dx.h, isso se torna muito mais intuitivo:

GradientStop stops[] =
{
  GradientStop(0.0f, COLOR_WHITE),
  GradientStop(1.0f, COLOR_BLUE),
};
auto collection = target.CreateGradientStopCollection(stops);
auto brush = target.CreateLinearGradientBrush(collection);

Embora não seja extremamente menor do que o código na Figura 1, é certamente mais fácil de escrever e bem menos provável que você erre na primeira vez, principalmente com a ajuda do IntelliSense. A biblioteca usa várias técnicas para produzir um modelo de programação mais agradável. Nesse caso, o método CreateGradientStopCollection é sobrecarregado com um modelo de função para deduzir o tamanho da matriz GradientStop em tempo de compilação, para que não seja necessário usar o macro _countof.

E quanto ao tratamento de erro? Um dos pré-requisitos para a produção de um modelo de programação tão conciso é a adoção de exceções para a propagação de erros. Considere a definição do método AsDxgi do dispositivo Direct3D que mencionei antes:

inline auto Device::AsDxgi() const -> Dxgi::Device2
{
  Dxgi::Device2 result;
  HR(m_ptr.CopyTo(result.GetAddressOf()));
  return result;
}

Esse é um padrão muito comum na biblioteca. Primeiro, observe que o método é const. Praticamente todos os métodos na biblioteca são const, pois o único membro de dados é o ComPtr subjacente e não há necessidade de modificá-lo. No corpo do método, você pode ver como o objeto Device resultante ganha vida. Todas as classes de bibliotecas fornecem uma semântica de movimentação, portanto, embora pareça realizar uma série de cópias — e, por inferência, uma série de pares AddRef/Release — na verdade, nada disso está acontecendo em tempo de execução. O HR que encapsula a expressão no meio é uma função embutida que gera uma exceção se o resultado não for S_OK. Por fim, a biblioteca sempre tentará retornar a classe mais específica para evitar que o chamador precise realizar chamadas adicionais para QueryInterface para expor mais funcionalidades.

O exemplo anterior usou o método ComPtr CopyTo, que efetivamente chama apenas QueryInterface. Veja outro exemplo do Direct2D:

inline auto BitmapBrush::GetBitmap() const -> Bitmap
{
  Bitmap result;
  (*this)->GetBitmap(result.GetAddressOf());
  return result;
}

Esse é um pouco diferente, pois chama diretamente um método na interface COM subjacente. Na verdade, esse padrão é o que constitui grande parte do código da biblioteca. Aqui, retorno o bitmap com o qual o pincel pinta. Muitos métodos do Direct2D retornam void, como é o caso aqui, portanto, não é preciso que a função HR verifique o resultado. No entanto, a indireção que leva ao método GetBitmap talvez não seja tão óbvia.

Enquanto criava o protótipo de versões anteriores da biblioteca, precisei escolher entre fazer truques com o compilador ou com o COM. Minhas tentativas anteriores envolveram fazer truques com o C++ usando modelos, especificamente características de tipo, mas também características de tipo do compilador (também conhecidas como características de tipo intrínseco). Foi divertido no início, mas logo percebi que estava gerando mais trabalho para mim.

Veja bem, a biblioteca modela o relacionamento “is-a” entre as interfaces COM como classes concretas. As interfaces COM podem herdar apenas diretamente de outra interface. Com exceção de IUnknown, cada interface COM deve herdar diretamente de outra interface. Por fim, isso encaminha a hierarquia de tipos de volta para IUnknown. Comecei definindo uma classe para cada interface COM. A classe RenderTarget continha um ponteiro de interface ID2D1RenderTarget. A classe DeviceContext continha um ponteiro de interface ID2D1DeviceContext. Isso parece razoável o bastante até que você queira tratar DeviceContext como RenderTarget. Afinal, a interface ID2D1DeviceContext é derivada da interface ID2D1RenderTarget. Seria bastante razoável ter DeviceContext e querer passá-la para um método que espera RenderTarget como um parâmetro de referência.

Infelizmente, o sistema de tipos C++ não concorda com isso. Usando essa abordagem, DeviceContext não pode realmente ser derivada de RenderTarget; se fosse, teria duas referências. Testei uma combinação de semântica de movimentação e características de tipo intrínseco para mover as referências, conforme necessário. Isso quase funcionou, mas houve casos em que um par AddRef/Release extra foi introduzido. Por fim, se tornou muito complexo, e uma solução mais simples era necessária.

Ao contrário do C++, o COM possui um contrato de binário bem definido. Afinal, é disso que se trata o COM. Contanto que você siga as regras estabelecidas, o COM não irá deixá-lo na mão. Você pode fazer truques com o COM, por assim dizer, e usar o C++ em seu favor, em vez de brigar com ele. Isso significa que cada classe C++ não mantém um ponteiro de interface COM fortemente tipado, mas apenas uma referência genérica para IUnknown. O C++ adiciona novamente a segurança de tipos, e suas regras para herança de classe — e, mais recentemente, semântica de movimentação — me permitem mais uma vez tratar esses “objetos” COM como classes C++. Conceitualmente, comecei com isto:

class RenderTarget { ComPtr<ID2D1RenderTarget> ptr; };
class DeviceContext { ComPtr<ID2D1DeviceContext> ptr; };

E terminei com isto:

class Object { ComPtr<IUnknown> ptr; };
class RenderTarget : public Object {};
class DeviceContext : public RenderTarget {};

Como a hierarquia lógica decorrente das interfaces COM e de seus relacionamentos agora está incorporada por um modelo de objeto C++, o modelo de programação como um todo está muito mais natural e utilizável. Há muitos outros detalhes, por isso, recomendo que você analise de perto o código-fonte. No momento da elaboração deste artigo, abrange praticamente todo o Direct2D e o Windows Animation Manager, além de partes úteis do DirectWrite, do Windows Imaging Component (WIC), do Direct3D e do DXGI. Além disso, adiciono funcionalidades regularmente, por isso, verifique com frequência. Aproveite!

Kenny Kerr é programador de computador, autor da Pluralsight e Microsoft MVP que mora no Canadá. Ele mantém um blog em kennykerr.ca e pode ser seguido no Twitter em twitter.com/kennykerr.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Worachai Chaoweeraprasit (Microsoft)
Worachai Chaoweeraprasit (Microsoft), wchao@microsoft.com

Worachai Chaoweeraprasit é o líder de desenvolvimento do Direct2D e do DirectWrite. É fanático pela velocidade e qualidade de gráficos vetoriais 2D, bem como pela legibilidade de textos na tela. Em seu tempo livre, gosta de ficar ao lado de seus dois filhos em casa.