Trabalhar com recursos de dispositivo DirectX
Entenda a função do Microsoft DirectX Graphics Infrastructure (DXGI) em seu jogo DirectX da Windows Store. O DXGI é um conjunto de APIs usadas para configurar e gerenciar recursos gráficos e de adaptador gráfico de baixo nível. Sem ele, você não teria como desenhar os gráficos do seu jogo em uma janela.
Pense no DXGI desta maneira: para acessar diretamente a GPU e gerenciar seus recursos, você deve ter uma maneira de descrevê-lo para seu aplicativo. A informação mais importante que você precisa sobre a GPU é o lugar para desenhar pixels para que ela possa enviar esses pixels para a tela. Normalmente, isso é chamado de "buffer de fundo" — um local na memória da GPU onde você pode desenhar os pixels e, em seguida, tê-los "invertidos" ou "trocados" e enviados para a tela em um sinal de atualização. O DXGI permite que você adquira esse local e os meios para usar esse buffer (chamado de cadeia de troca porque é uma cadeia de buffers permutáveis, permitindo várias estratégias de buffer).
Para fazer isso, você precisa de acesso para gravar na cadeia de permuta e um identificador para a janela que exibirá o buffer traseiro atual para a cadeia de permuta. Você também precisa conectar os dois para garantir que o sistema operacional atualize a janela com o conteúdo do buffer traseiro quando você solicitar isso.
O processo geral para desenhar na tela é o seguinte:
- Obtenha um CoreWindow para seu aplicativo.
- Obtenha uma interface para o dispositivo Direct3D e o contexto.
- Crie a cadeia de permuta para exibir sua imagem renderizada no CoreWindow.
- Crie um destino de renderização para desenho e preencha-o com pixels.
- Apresente a cadeia de troca!
Criar uma janela para seu aplicativo
A primeira coisa que precisamos fazer é criar uma janela. Primeiro, crie uma classe de janela preenchendo uma instância de WNDCLASS e, em seguida, registre-a usando RegisterClass. A classe window contém propriedades essenciais da janela, incluindo o ícone que ela usa, a função de processamento de mensagem estática (mais sobre isso mais tarde) e um nome exclusivo para a classe window.
if(m_hInstance == NULL)
m_hInstance = (HINSTANCE)GetModuleHandle(NULL);
HICON hIcon = NULL;
WCHAR szExePath[MAX_PATH];
GetModuleFileName(NULL, szExePath, MAX_PATH);
// If the icon is NULL, then use the first one found in the exe
if(hIcon == NULL)
hIcon = ExtractIcon(m_hInstance, szExePath, 0);
// Register the windows class
WNDCLASS wndClass;
wndClass.style = CS_DBLCLKS;
wndClass.lpfnWndProc = MainClass::StaticWindowProc;
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = m_hInstance;
wndClass.hIcon = hIcon;
wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = m_windowClassName.c_str();
if(!RegisterClass(&wndClass))
{
DWORD dwError = GetLastError();
if(dwError != ERROR_CLASS_ALREADY_EXISTS)
return HRESULT_FROM_WIN32(dwError);
}
Em seguida, você cria a janela. Também precisamos fornecer informações de tamanho para a janela e o nome da classe de janela que acabamos de criar. Ao chamar CreateWindow, você obtém de volta um ponteiro opaco para a janela chamada HWND; você precisará manter o ponteiro HWND e usá-lo sempre que precisar fazer referência à janela, incluindo destruí-la ou recriá-la, e (especialmente importante) ao criar a cadeia de permuta DXGI que você usa para desenhar na janela.
m_rc;
int x = CW_USEDEFAULT;
int y = CW_USEDEFAULT;
// No menu in this example.
m_hMenu = NULL;
// This example uses a non-resizable 640 by 480 viewport for simplicity.
int nDefaultWidth = 640;
int nDefaultHeight = 480;
SetRect(&m_rc, 0, 0, nDefaultWidth, nDefaultHeight);
AdjustWindowRect(
&m_rc,
WS_OVERLAPPEDWINDOW,
(m_hMenu != NULL) ? true : false
);
// Create the window for our viewport.
m_hWnd = CreateWindow(
m_windowClassName.c_str(),
L"Cube11",
WS_OVERLAPPEDWINDOW,
x, y,
(m_rc.right-m_rc.left), (m_rc.bottom-m_rc.top),
0,
m_hMenu,
m_hInstance,
0
);
if(m_hWnd == NULL)
{
DWORD dwError = GetLastError();
return HRESULT_FROM_WIN32(dwError);
}
O modelo de aplicativo da área de trabalho do Windows inclui um gancho no loop de mensagens do Windows. Você precisará basear seu loop de programa principal fora desse gancho escrevendo uma função "StaticWindowProc" para processar eventos de janela. Essa deve ser uma função estática porque o Windows a chamará fora do contexto de qualquer instância de classe. Aqui está um exemplo muito simples de uma função de processamento de mensagens estáticas.
LRESULT CALLBACK MainClass::StaticWindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch(uMsg)
{
case WM_CLOSE:
{
HMENU hMenu;
hMenu = GetMenu(hWnd);
if (hMenu != NULL)
{
DestroyMenu(hMenu);
}
DestroyWindow(hWnd);
UnregisterClass(
m_windowClassName.c_str(),
m_hInstance
);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
Este exemplo simples verifica apenas as condições de saída do programa: WM_CLOSE, enviado quando a janela é solicitada a ser fechada, e WM_DESTROY, que é enviado depois que a janela é realmente removida da tela. Um aplicativo de produção completo também precisa lidar com outros eventos de janela — para obter a lista completa de eventos de janela, consulte Notificações de janela.
O próprio loop do programa principal precisa reconhecer as mensagens do Windows, permitindo que o Windows tenha a oportunidade de executar o proc de mensagem estática. Ajude o programa a ser executado de forma eficiente forjando o comportamento: cada iteração deve optar por processar novas mensagens do Windows se elas estiverem disponíveis e, se nenhuma mensagem estiver na fila, ela deve renderizar um novo quadro. Aqui está um exemplo muito simples:
bool bGotMsg;
MSG msg;
msg.message = WM_NULL;
PeekMessage(&msg, NULL, 0U, 0U, PM_NOREMOVE);
while (WM_QUIT != msg.message)
{
// Process window events.
// Use PeekMessage() so we can use idle time to render the scene.
bGotMsg = (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE) != 0);
if (bGotMsg)
{
// Translate and dispatch the message
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// Update the scene.
renderer->Update();
// Render frames during idle time (when no messages are waiting).
renderer->Render();
// Present the frame to the screen.
deviceResources->Present();
}
}
Obter uma interface para o dispositivo Direct3D e o contexto
A primeira etapa para usar o Direct3D é adquirir uma interface para o hardware Direct3D (a GPU), representada como instâncias de ID3D11Device e ID3D11DeviceContext. O primeiro é uma representação virtual dos recursos da GPU, e o segundo é uma abstração independente de dispositivo do pipeline e processo de renderização. Aqui está uma maneira fácil de pensar nisso: ID3D11Device contém os métodos gráficos que você chama com pouca frequência, geralmente antes de qualquer renderização ocorrer, para adquirir e configurar o conjunto de recursos que você precisa para começar a desenhar pixels. ID3D11DeviceContext, por outro lado, contém os métodos que você chama cada quadro: carregando em buffers e exibições e outros recursos, alterando a fusão de saída e o estado do rasterizador, gerenciando sombreadores e desenhando os resultados da passagem desses recursos pelos estados e sombreadores.
Há uma parte muito importante desse processo: definir o nível do recurso. O nível de recurso informa ao DirectX o nível mínimo de hardware compatível com seu aplicativo, com D3D_FEATURE_LEVEL_9_1 como o conjunto de recursos mais baixo e D3D_FEATURE_LEVEL_11_1 como o mais alto atual. Você deve oferecer suporte ao 9_1 como mínimo se quiser alcançar o maior público possível. Reserve algum tempo para ler sobre níveis de recursos o Direct3D e avaliar por si mesmo os níveis mínimos e máximos de recursos que você deseja que seu jogo ofereça suporte e entender as implicações de sua escolha.
Obtenha referências (ponteiros) ao contexto do dispositivo Direct3D e do dispositivo e armazene-as como variáveis de nível de classe na instância DeviceResources (como ponteiros inteligentes ComPtr). Use essas referências sempre que precisar acessar o dispositivo Direct3D ou o contexto do dispositivo.
D3D_FEATURE_LEVEL levels[] = {
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,
};
// This flag adds support for surfaces with a color-channel ordering different
// from the API default. It is required for compatibility with Direct2D.
UINT deviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if defined(DEBUG) || defined(_DEBUG)
deviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
// Create the Direct3D 11 API device object and a corresponding context.
Microsoft::WRL::ComPtr<ID3D11Device> device;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;
hr = D3D11CreateDevice(
nullptr, // Specify nullptr to use the default adapter.
D3D_DRIVER_TYPE_HARDWARE, // Create a device using the hardware graphics driver.
0, // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
deviceFlags, // Set debug and Direct2D compatibility flags.
levels, // List of feature levels this app can support.
ARRAYSIZE(levels), // Size of the list above.
D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for Windows Store apps.
&device, // Returns the Direct3D device created.
&m_featureLevel, // Returns feature level of device created.
&context // Returns the device immediate context.
);
if (FAILED(hr))
{
// Handle device interface creation failure if it occurs.
// For example, reduce the feature level requirement, or fail over
// to WARP rendering.
}
// Store pointers to the Direct3D 11.1 API device and immediate context.
device.As(&m_pd3dDevice);
context.As(&m_pd3dDeviceContext);
Criar a cadeia de permuta
Ok: você tem uma janela para desenhar e tem uma interface para enviar dados e dar comandos para a GPU. Agora vamos ver como uni-los.
Primeiro, você diz ao DXGI quais valores usar para as propriedades da cadeia de permuta. Faça isso usando uma estrutura DXGI_SWAP_CHAIN_DESC. Seis campos são particularmente importantes para aplicativos de desktop:
- Janela: indica se a cadeia de permuta está em tela cheia ou cortada na janela. Defina isso como TRUE para colocar a cadeia de permuta na janela criada anteriormente.
- BufferUsage: defina como DXGI_USAGE_RENDER_TARGET_OUTPUT. Isso indica que a cadeia de permuta será uma superfície de desenho, permitindo que você a use como um destino de renderização Direct3D.
- SwapEffect: defina como DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL.
- Formato: o formato DXGI_FORMAT_B8G8R8A8_UNORM especifica a cor de 32 bits: 8 bits para cada um dos três canais de cores RGB e 8 bits para o canal alfa.
- BufferCount: defina como 2 para um comportamento tradicional de buffer duplo para evitar divisões. Defina a contagem de buffers como 3 se o conteúdo gráfico precisar de mais de um ciclo de atualização do monitor para renderizar um único quadro (em 60 Hz, por exemplo, o limite é superior a 16 ms).
- SampleDesc: este campo controla a multiamostragem. Defina Contagem como 1 e Qualidade como 0 para cadeias de permuta de modelo flip. (Para usar multiamostragem com cadeias de permuta de modelo flip, desenhe em um destino de renderização de várias amostras separado e, em seguida, resolva esse destino para a cadeia de permuta antes de apresentá-lo. O código de exemplo é fornecido em Multisampling em aplicativos da Windows Store.)
Depois de especificar uma configuração para a cadeia de permuta, você deve usar a mesma fábrica DXGI que criou o dispositivo Direct3D (e o contexto do dispositivo) para criar a cadeia de permuta.
Forma curta:
Obtenha a referência ID3D11Device que você criou anteriormente. Faça o upcast para IDXGIDevice3 (se ainda não o fez) e, em seguida, chame IDXGIDevice::GetAdapter para adquirir o adaptador DXGI. Obtenha a fábrica pai desse adaptador chamando IDXGIAdapter::GetParent (IDXGIAdapter herda de IDXGIObject) — agora você pode usar essa fábrica para criar a cadeia de permuta chamando CreateSwapChainForHwnd, conforme visto no exemplo de código a seguir.
DXGI_SWAP_CHAIN_DESC desc;
ZeroMemory(&desc, sizeof(DXGI_SWAP_CHAIN_DESC));
desc.Windowed = TRUE; // Sets the initial state of full-screen mode.
desc.BufferCount = 2;
desc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.SampleDesc.Count = 1; //multisampling setting
desc.SampleDesc.Quality = 0; //vendor-specific flag
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
desc.OutputWindow = hWnd;
// Create the DXGI device object to use in other factories, such as Direct2D.
Microsoft::WRL::ComPtr<IDXGIDevice3> dxgiDevice;
m_pd3dDevice.As(&dxgiDevice);
// Create swap chain.
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
Microsoft::WRL::ComPtr<IDXGIFactory> factory;
hr = dxgiDevice->GetAdapter(&adapter);
if (SUCCEEDED(hr))
{
adapter->GetParent(IID_PPV_ARGS(&factory));
hr = factory->CreateSwapChain(
m_pd3dDevice.Get(),
&desc,
&m_pDXGISwapChain
);
}
Se você está apenas começando, provavelmente é melhor usar a configuração mostrada aqui. Agora, neste ponto, se você já está familiarizado com versões anteriores do DirectX, você pode estar se perguntando: "Por que não criamos o dispositivo e a cadeia de troca ao mesmo tempo, em vez de voltar a todas essas classes?" A resposta é a eficiência: as cadeias de permuta são recursos de dispositivo Direct3D e os recursos de dispositivo estão vinculados ao dispositivo Direct3D específico que os criou. Se você criar um novo dispositivo com uma nova cadeia de permuta, será necessário recriar todos os recursos do dispositivo usando o novo dispositivo Direct3D. Assim, criando a cadeia de permuta com a mesma fábrica (como mostrado acima), você é capaz de recriar a cadeia de permuta e continuar usando os recursos do dispositivo Direct3D que você já carregou!
Agora você tem uma janela do sistema operacional, uma maneira de acessar a GPU e seus recursos e uma cadeia de troca para exibir os resultados de renderização. Tudo o que resta é juntar a coisa toda!
Criar um destino de renderização para desenho
O pipeline de sombreador precisa de um recurso para desenhar pixels. A maneira mais simples de criar esse recurso é definir um recurso ID3D11Texture2D como um buffer traseiro para o sombreador de pixel desenhar e, em seguida, ler essa textura na cadeia de permuta.
Para fazer isso, crie um exibição do destino de renderização. No Direct3D, um modo de exibição é uma maneira de acessar um recurso específico. Nesse caso, a exibição permite que o sombreador de pixel grave na textura à medida que conclui suas operações por pixel.
Vejamos o código para isso. Quando você define DXGI_USAGE_RENDER_TARGET_OUTPUT na cadeia de permuta, isso permite que o recurso Direct3D subjacente seja usado como uma superfície de desenho. Então, para obter nossa exibição de destino de renderização, só precisamos obter o buffer de volta da cadeia de permuta e criar uma exibição de destino de renderização vinculada ao recurso de buffer de retorno.
hr = m_pDXGISwapChain->GetBuffer(
0,
__uuidof(ID3D11Texture2D),
(void**) &m_pBackBuffer);
hr = m_pd3dDevice->CreateRenderTargetView(
m_pBackBuffer.Get(),
nullptr,
m_pRenderTarget.GetAddressOf()
);
m_pBackBuffer->GetDesc(&m_bbDesc);
Crie também um buffer de estêncil de profundidade. Um buffer de estêncil de profundidade é apenas uma forma particular de recurso ID3D11Texture2D, que normalmente é usado para determinar quais pixels têm prioridade de desenho durante a rasterização com base na distância dos objetos na cena da câmera. Um buffer de estêncil de profundidade também pode ser usado para efeitos de estêncil, onde pixels específicos são descartados ou ignorados durante a rasterização. Esse buffer deve ter o mesmo tamanho que o destino de renderização. Observe que não é possível ler ou renderizar a textura de estêncil de profundidade do buffer de quadros porque ela é usada exclusivamente pelo pipeline de sombreador antes e durante a rasterização final.
Crie também uma exibição para o buffer de estêncil de profundidade como um ID3D11DepthStencilView. A exibição informa ao pipeline do sombreador como interpretar o recurso subjacente ID3D11Texture2D - portanto, se você não fornecer essa exibição, nenhum teste de profundidade por pixel será realizado, e os objetos em sua cena podem parecer um pouco de dentro para fora, no mínimo!
CD3D11_TEXTURE2D_DESC depthStencilDesc(
DXGI_FORMAT_D24_UNORM_S8_UINT,
static_cast<UINT> (m_bbDesc.Width),
static_cast<UINT> (m_bbDesc.Height),
1, // This depth stencil view has only one texture.
1, // Use a single mipmap level.
D3D11_BIND_DEPTH_STENCIL
);
m_pd3dDevice->CreateTexture2D(
&depthStencilDesc,
nullptr,
&m_pDepthStencil
);
CD3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc(D3D11_DSV_DIMENSION_TEXTURE2D);
m_pd3dDevice->CreateDepthStencilView(
m_pDepthStencil.Get(),
&depthStencilViewDesc,
&m_pDepthStencilView
);
A última etapa é criar um visor. Isso define o retângulo visível do buffer traseiro exibido na tela; Você pode alterar a parte do buffer que é exibida na tela alterando os parâmetros do visor. Esse código tem como alvo todo o tamanho da janela — ou a resolução da tela, no caso de cadeias de permuta de tela cheia. De bobeira, altere os valores de coordenadas fornecidos e observe os resultados.
ZeroMemory(&m_viewport, sizeof(D3D11_VIEWPORT));
m_viewport.Height = (float) m_bbDesc.Height;
m_viewport.Width = (float) m_bbDesc.Width;
m_viewport.MinDepth = 0;
m_viewport.MaxDepth = 1;
m_pd3dDeviceContext->RSSetViewports(
1,
&m_viewport
);
E é assim que você começa do zero e desenha pixels em uma janela! Ao começar, é uma boa ideia se familiarizar com a forma como o DirectX, por meio do DXGI, gerencia os recursos principais necessários para começar a desenhar pixels.
Em seguida, você verá a estrutura do pipeline gráfico; consulte Compreender o pipeline de renderização do modelo de aplicativo DirectX.
Tópicos relacionados