Inicializando Direct3D – Parte 2/2

Publicado el Lunes 26 julio 2010 en DirectX por Thund

En la primera mitad de este post presenté las nuevas interfaces de Direct3D y DXGI necesarias para empezar a mostrar imágenes por pantalla. Ahora voy a mostrar cómo se utilizan, de forma básica.

Paso 0: Dependencias

Para este ejemplo, necesitaremos estas librerías de DirectX 11: d3d11.lib y dxgi.lib

Incluiremos en nuestro código el siguiente archivo de cabecera: d3d11.h

#include <d3d11.h>

Nótese que tal archivo de cabecera ya incluye a dxgi.h (además de d3d10_1.h, para quien le interese saberlo).

Paso 1: En busca del adaptador

Lo primero que haremos será seleccionar un dispositivo gráfico de entre los que tengamos instalados en nuestro ordenador. Como la mayoría somos mileuristas, lo normal es que sólo tengamos uno, así que elegiremos el primero que aparezca. La forma de hacerlo es usando una de las interfaces más importantes en la API de DXGI: IDXGIFactory. Ésta interfaz de nombre tan original se encarga de la creación de swap chains y adaptadores; para instanciarla se utiliza la función global CreateDXGIFactory. Un adaptador gráfico físico está representado por la interfaz IDXGIAdapter, y se obtiene mediante el método CreateSoftwareAdapter de IDXGIFactory en el caso de necesitar un adaptador por software, o a través de la enumeración de todos los adaptadores hardware disponibles, como es lo habitual, con el método EnumAdapter de la misma interfaz.

IDXGIFactory* pFactory = NULL;
CreateDXGIFactory(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&pFactory));

IDXGIAdapter* pAdapter = NULL;
pFactory->EnumAdapters(0, &pAdapter);

Más de uno estará ahora mismo pensando qué cojones es esa función de nombre tan típico de Microsoft… os ahorraré la búsqueda. La función __uuidof se encarga de obtener un GUID (Globally Unique IDentifier) a partir de un tipo, una referencia, un puntero, una variable, un array o una plantilla especializada; en nuestro caso, necesitamos pasar como primer parámetro el GUID del tipo IDXGIFactory.

El método EnumAdapters rellena el puntero con la dirección del IDXGIAdapter que indiquemos con el índice del primer parámetro; aquí usamos el 0 (cero) puesto que sabemos que sólo tenemos un dispositivo. En caso de que pasemos un índice igual o superior al número de adaptadores, el método devolverá el código de error DXGI_ERROR_NOT_FOUND. Cuando necesitemos enumerar todos los adaptadores, nos valdremos de tal resultado como condición para iterar.

Éste no es un paso totalmente necesario, como podrá verse más abajo, pero en cierto modo es ilustrativo.

Paso 2: Swap chain a medida

Antes de montar la swap chain hay que definirla, esto es, determinar las dimensiones de los buffers, el formato de sus texels, la frecuencia del swapping (intercambio)… Exactamente lo mismo que hacíamos con DirectX 9, como ya mencioné, cuando usábamos la estructura D3DPRESENT_PARAMETERS, que ahora se llama DXGI_SWAP_CHAIN_DESC. Pasaré ahora a describir brevemente los atributos:

DXGI_SWAP_CHAIN_DESC swapChainDesc;

swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferDesc.Height = 600;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
swapChainDesc.BufferDesc.Width = 800;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferCount = 1;
swapChainDesc.OutputWindow = windowsHelper->GetWindowHandler();
swapChainDesc.Windowed = true;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
swapChainDesc.Flags = 0;

El formato (aributo BufferDesc.Format) está formado, normalmente, por 32 bits divididos en grupos de 8 con el orden RGBA. El alto y el ancho (BufferDesc.Height y BufferDesc.Width) se refieren a la resolución de los buffers, en píxeles. La tasa de refresco (BufferDesc.RefreshRate) es un número racional del cual especificamos el numerador y el denominador; como mi monitor es un TFT, he puesto 60 / 1 = 60 hz. Tenemos que elegir cómo se dibujará la imagen en el monitor cuando la resolución del mismo sea mayor que la del front buffer: si cambiará su tamaño para ajustarse (DXGI_MODE_SCALING_STRETCHED) o si aparecerá el recuadro centrado en la pantalla (DXGI_MODE_SCALING_CENTERED). Como me la suda lo que ocurra, he dejado el atributo BufferDesc.Scaling sin especificar (DXGI_MODE_SCALING_UNSPECIFIED). El siguiente atributo (BufferDesc.ScalineOrdering), he de reconocer que no conozco su utilidad exacta, aunque se puede deducir que tiene que ver con el orden con que el rasterizador dibuja las líneas de píxeles horizontales que forman la imagen. No vamos a usar antialiasing por lo que ajustamos el número de multisamples por píxel (SampleDesc.Count) a 1 y la calidad (SampleDesc.Quality) del mismo a 0 (cero). Las texturas que actúan como buffers serán usadas (BufferUsage) como objetivos para la renderización, por lo que elegimos el valor DXGI_USAGE_RENDER_TARGET_OUTPUT (la otra única opción posible es DXGI_USAGE_SHADER_INPUT). Cuando vi esto último quedé extrañado, estaba convencido de que habría que usar el valor DXGI_USAGE_BACK_BUFFER, pero da la impresión de que su uso es muy específico. Indicamos ahora la cantidad de buffers (BufferCount) que compondrán nuestra swap chain; aquí habría que darle una colleja a alguno de Microsoft, porque la documentación está mal. Claramente dice que en el número hay que incluir al front buffer, o sea, front buffer + back buffer = 2 buffers. Pues no, si sólo vamos a usar un back buffer, el resultado es 1; el front buffer no hay que contarlo para nada. Cada swap chain está vinculada desde su creación a una ventana y un dispositivo y deberá ser creada de nuevo si se quiere enlazar a otros; le pasamos el manejador de la única ventana que tenemos ahora mismo. Establecemos que queremos renderizar en modo ventana (Windowed), que es más fácil depurar. El efecto del intercambio (SwapEffect) es lo que ocurrirá con el back buffer cuando llamemos al método Present; nosotros no vamos a hacer nada en especial con él, por lo que lo descartamos (DXGI_SWAP_EFFECT_DISCARD). Por último, no queremos ajustar ninguna opción especial de la swap chain, así que ponemos los Flags a 0 (cero).

Paso 3: Enhorabuena, ha tenido usted trillizos

Una vez rellenado el bloque de configuración de la swap chain, aún quedan más parámetros que definir. Qué coñazo ¿no? Hay que elegir el tipo de driver, la capa que queremos activar, algunos flags… y el nivel de funcionalidad (feature level). ¿Qué carajo es eso de los feature levels? Pues bien, son un gran avance en la cohesión entre proveedores de hardware y los desarrolladores de APIs gráficas de bajo nivel, impulsado desde DirectX 10 (incluso antes, desde la aparición del Shader Model 3.0 en DirectX 9). Consiste en cargarse los flags de los Device Capabilities (a.k.a. CAPS) con los que había que pelearse en DirectX 9, y reemplazarlos por una especie de "contratos" que aseguran que un determinado dispositivo provee de un paquete de funcionalidades, según la versión de DirectX que las soporta. Así, cuando compramos una tarjeta gráfica con la etiqueta "DirectX 11" en la caja, quiere decir que implementa el conjunto mínimo establecido de las características que ofrece DirectX 11. Aún tendremos que hacer ciertas comprobaciones sobre la capacidad del hardware mediante ciertos métodos de la interfaz IDXGIOutput, como veremos en posts posteriores, pero ya no hay que ponerse a enmascarar ristras de bits para saber si la tarjeta soporta T&L, ciertos modos de presentación, algunas operaciones del stencil buffer, etc. Hay que decir que la selección explícita del nivel de funcionalidad es algo nuevo en DirectX 11 que permite orientar la implementación de nuestro programa a un determinado conjunto de hardware. Cuando especificamos que queremos crear un manejador de dispositivo para DirectX 9, lo que ocurrirá es que las instrucciones que demos con interfaces de DirectX 11 se traducirán a las que lanzaría DirectX 9 al dispositivo (lo cual conlleva una penalización de rendimiento que no tendríamos si usáramos la API de DirectX 9 directamente). Ésto que parece tan bonito también tiene su aquél, ya que hay algunas características que no pueden ser traducidas, pues no existen en el hardware, como el soporte de arrays de texturas o el uso de DirectCompute. De modo que habrá que seguir haciendo comprobaciones durante el desarrollo para evitar que ciertas cosas les exploten en la cara a los usuarios de tarjetas de video obsoletas. Podéis encontrar una tabla con todas las incompatibilidades en la documentación.

ID3D11Device* pDevice = NULL;
IDXGISwapChain* pSwapChain = NULL;
ID3D11DeviceContext* pImmediateContext = NULL;

D3D_FEATURE_LEVEL featureLevels[1] = { D3D_FEATURE_LEVEL_10_1 };

D3D_FEATURE_LEVEL* pFeatureLevelsOut = NULL;

D3D11CreateDeviceAndSwapChain(	pAdapter,
								D3D_DRIVER_TYPE_UNKNOWN,
								NULL,
								D3D11_CREATE_DEVICE_DEBUG,
								featureLevels,
								1,
								D3D11_SDK_VERSION,
								&swapChainDesc,
								&pSwapChain,
								&pDevice,
								pFeatureLevelsOut,
								&pImmediateContext );

El código precedente comienza declarando e inicializando punteros a las 3 interfaces de los elementos principales que necesitamos para renderizar: el dispositivo, la swap chain y el contexto del dispositivo que, como puede deducirse del nombre de la variable, será un contexto inmediato. A continuación, rellenamos un array de niveles de funcionalidad con un único elemento, que hace referencia a la versión 10.1 de DirectX. Es un nivel que funciona en mi máquina, puede cambiarse por cualquier otro o incluso usar un puntero nulo en lugar de un array, lo que significa que se usará la versión más avanzada compatible con el sistema. La siguiente declaración es un puntero que será asignado internamente, en el momento de la creación de los elementos, a una lista de niveles de funcionalidad soportados por el dispositivo, es decir, actuará como parámetro de salida. Por fin, usamos la función global D3D11CreateDeviceAndSwapChain para reunirlo todo y crear los 3 objetos ya mencionados. Podría crear el manejador del dispositivo y el contexto por un lado (función D3D11CreateDevice), y la swap chain por otro (método CreateSwapChain de la interfaz IDXGIFactory), pero ya que se nos facilita un todo-en-uno ahorraremos líneas.

Voy a comentar cada uno de los parámetros de la función refiriéndome al número de línea en el que aparecen, por facilidad. Como primer parámetro, en la línea #09 utilizo el puntero a la interfaz del adaptador que seleccionamos en el paso 1; podría pasar un puntero nulo, en cuyo caso se usaría el adaptador por defecto. El siguiente parámetro, en la línea #10, es el tipo de driver que será creado (hardware, WARP, de referencia…); tiene como restricción que, si hemos pasado un puntero no nulo como adaptador, el tipo de driver debe ser "desconocido" (D3D_DRIVER_TYPE_UNKNOWN); si el puntero era nulo, forzosamente deberemos elegir entre un driver de tipo hardware (D3D_DRIVER_TYPE_HARDWARE) o software (D3D_DRIVER_TYPE_SOFTWARE). En la línea #11 hay que proporcionar un manejador de la DLL del rasterizador por software, si es que vamos a usar un driver de tipo software; si el driver era de tipo distinto de software, obligatoriamente hay que usar un puntero nulo. Quiero disponer de toda la información posible durante el proceso de desarrollo y depuración, por lo que, mediante el flag de la línea #12, activo la capa de depuración de DirectX. Seguidamente, establezco los niveles de funcionalidad (#13) y el número de elementos de tal array (#14); si pasara un puntero nulo, se usaría internamente un array con todos los niveles (el número de elementos tendría que ser cero). Tras indicar la versión del SDK (#15), suministramos la estructura con las características de la swap chain (#16) que rellenamos en el paso 2 y, a continuación, todos los parámetros de salida. Si todo sale bien, la función devolverá el código S_OK.

Paso 4: Y se hizo la luz

Gracias al mecanismo que acabamos de construir, tenemos total control sobre el adaptador. Sin embargo, para poder enviar las órdenes de renderizado, hay que crear un canal que nos comunique con los recursos, esto es, una vista. Para este ejemplo, lo único que vamos a hacer es pintar la pantalla de un color, para lo cual crearemos una vista del back buffer.

ID3D11Texture2D* pBackBuffer = NULL;
pSwapChain->GetBuffer(0, __uuidof(*pBackBuffer), reinterpret_cast<void**>(&pBackBuffer));

ID3D11RenderTargetView* pRenderTargetView = NULL;
pDevice->CreateRenderTargetView( dynamic_cast<ID3D11Resource*>(pBackBuffer), NULL, &pRenderTargetView );

Como puede verse, primero extraemos la textura que hace de back buffer de nuestra swap chain y, a continuación, la usamos para generar una vista a la misma. Lo único que puedo remarcar aquí es que el índice del back buffer es el 0 (cero) debido a que es el único que hay y el efecto de intercambio de la swap chain no es secuencial (DXGI_SWAP_EFFECT_SEQUENTIAL).

Ya sólo queda enviar los comandos para que el dispositivo rellene el back buffer del color que queramos y lo presente al monitor en forma de ventana. Para ello hay que organizar el código del hilo perteneciente a la ventana, de manera que en el bucle de mensajes de Windows se llame a los procesos de renderización. Describir el código es trivial, pero por no perder las costumbres… Lo primero que hago es llamar al método ClearRenderTargetView del contexto inmediato, pasándole la vista del back buffer y el color, y luego, simplemente, ordeno a la swap chain que ponga el back buffer en el lugar del front buffer, enviándolo a la pantalla.

Editado 04/08/2010 – Ver Antiguo

En el caso que nos ocupa, se renderizará a toda máquina, sin límite de FPS (Frames Per Second), siempre que no llegue el mensaje WM_QUIT. Se usa PeekMessage en lugar de GetMessage debido a que ésta última accede a la cola de mensajes del hilo y bloquea la ejecución hasta que capture uno, mientras que PeekMessage vuelve inmediatamente, haya mensajes en la cola o no.

// Main message loop:
while ( msg.message != WM_QUIT )
{
    if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) )
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    else
    {
        // Render
        pImmediateContext->ClearRenderTargetView(pRenderTargetView, backColor);
        pSwapChain->Present(0, 0);
    }
}

Quisiera explicar por encima los parámetros del método Present y sus consecuencias. El primero se encarga de desactivar la sincronización vertical (Vsync) mediante el valor 0 (cero), o de activarla, especificando el número de refrescos del monitor que la swap chain esperará antes de presentar el back buffer. El segundo parámetro es un flag que tiene que ver con 2 cosas: una, el modo de presentar los buffers de la swap chain (renderizar un frame por buffer o sólo el primer buffer), y otra, el test de presentación. Ya os lo estaréis oliendo, el flag DXGI_PRESENT_TEST es el sustituto (en parte) del antiguo método TestCooperativeLevel. Cuando pasamos éste valor al método Present no se enviará nada a la pantalla y obtendremos un código de resultado que, según el caso, puede informarnos de que nuestra ventana está siendo totalmente eclipsada por otra (DXGI_STATUS_OCCLUDED), que se ha cambiado la resolución (DXGI_STATUS_MODE_CHANGED), o si ha ocurrido algo extraño y hay que volver a crear el manejador del dispositivo (DXGI_ERROR_DEVICE_RESET), por ejemplo. Una gran novedad respecto a éste último tipo de error es que ya no hay que partirse el pecho volviendo a crear todos los recursos dependientes de un dispositivo para cambiar de modo ventana a pantalla completa, ya no existen OnResetDevice y OnLostDevice, sino que ahora basta con llamar al método SetFullscreenState de la interfaz de la swap chain. Imagino, y remarco que esto es una suposición porque a día de hoy no lo he probado, que si se diera el error DXGI_ERROR_DEVICE_RESET, sí que habría que montar todo el chiringuito de nuevo. Me permito hacer especulaciones, aunque no me gusten, porque lo comprobaremos en breve. Hay que anotar en la cabeza que nuestra aplicación siempre debe tener un modo de espera, es decir, un estado en el que no se realice ningún tipo de tarea relacionada con la renderización, de manera que no se consuman recursos tontamente cuando, por ejemplo, nuestra ventana esté siendo ocultada por otra.

No nos olvidemos de que todos los objetos COM que hemos creado deben ser liberados mediante el correspondiente método Release de cada uno. Un detalle importante es que nunca debe liberarse la swap chain mientras el modo de visualización sea pantalla completa, pues eso causará un error de los chungos. Hay que cambiar a modo ventana antes de destruirla. Ahí va una espectacular captura del resultado del ejemplo:

[Imagen ventana verde]

Sumario de Portabilidad

DirectX 9 DirectX 11 Comentario
Direct3DCreate9 D3D11CreateDeviceAndSwapChain  
IDirect3DDevice9 ID3D11Device  
IDirect3D9   No hay análogo.
IDirect3DSwapChain9 IDXGISwapChain  
IDirect3DTexture9 ID3D11Texture2D  
IDirect3DCubeTexture9 ID3D11Texture3D  
TestCooperativeLevel DXGI_PRESENT_TEST Es un flag del método IDXGISwapChain::Present.
D3DPRESENT_PARAMETERS DXGI_SWAP_CHAIN_DESC  
IDirect3DResource9 ID3D11Resource  
IDirect3DSwapChain9::GetBackBuffer IDXGISwapChain::GetBuffer  
IDirect3DDevice9::Present IDXGISwapChain::Present  
IDirect3DDevice9::Clear ID3D11DeviceContext::ClearRenderTargetView  

 

Descargas

Podéis descargar el código de ejemplo desde aquí:

EndlessGameDX11Init.zip (23 Kb)
 

Etiquetas: , ,

2 Respuestas a 'Inicializando Direct3D – Parte 2/2'

Suscríbete a los comentarios con RSS o TrackBack a 'Inicializando Direct3D – Parte 2/2'.

  1. Thund dijo:

    Corregido un error en el uso del bucle de mensajes de Windows.

    on agosto 4th, 2010 at 22:05

  2. [...] EndlessGameLectura recomendadaMass Effect 2Star Craft II – Released!Inicializando Direct3D – Parte 2/2Enlaces [...]

    on septiembre 25th, 2010 at 14:03

Publica un comentario