Un vértice, dos vértices, tres vértices ¡coño un triángulo!

Publicado el Sábado 25 septiembre 2010 en DirectX por Thund

Ha pasado más tiempo del que me gustaría desde que escribí el anterior post sobre la inicialización de Direct3D, pero es que hacía mucha calor para ponerme a redactar… espero me comprendan, ejem. De hecho, el código expuesto aquí lleva escrito desde hace un mes. Pero vamos al lío, que no hay tiempo que perder. En éste post mostraré el tortuoso camino hacia la visualización de elementos geométricos, desde donde lo dejamos.

Hasta ahora teníamos 4 interfaces de objetos correctamente inicializados: ID3D11Device, IDXGISwapChain, ID3D11DeviceContext e ID3D11RenderTarget. Esto era suficiente para poder arrancar la maquinaria de Direct3D y rellenar el área de cliente de la ventana con un color para probarlo; sin embargo, en el paso 4, que era el último, sería lógico incluir algo que necesitaremos en adelante, que es la asociación del render target al device context:

pImmediateContext->OMSetRenderTargets(1, &pRenderTargetView, NULL);

Paso 5: El ojo de buey

Es necesario definir sobre qué porción de nuestra ventana será proyectado el front buffer. Antes se utilizaba la estructura D3DVIEWPORT9 para empaquetar los parámetros del viewport, ahora utilizamos D3D11_VIEWPORT, que ni quita ni pone nada nuevo.

D3D11_VIEWPORT viewport;
viewport.Height   = 600.0f;
viewport.Width    = 800.0f;
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
viewport.MaxDepth = 1.0f;
viewport.MinDepth = 0.0f;

pImmediateContext->RSSetViewports(1, &viewport);

Paso 6: El átomo

Como bien sabe el lector, el universo tridimensional que vemos por pantalla está formado en su inmensa mayoría por vértices que, ordenados de cierta manera, definen superficies triangulares que, a su vez, pueden formar mallas. Toda la parafernalia geométrica puede encontrarse fácilmente en Internet y no es el objeto de éste blog por el momento, así que me lo salto. El hecho es que, tal como ocurre con los átomos tradicionales, no todos los vértices son iguales; podemos definir un vértice al cual afecte la luz, que pueda ser envuelto por una textura o que sea representado por un color determinado. O podemos diseñar otro para el cual las anteriores propiedades no tengan sentido.

struct MyVertexType
{
    XMFLOAT4 position;
    XMFLOAT4 color;
};

D3D11_INPUT_ELEMENT_DESC arElements[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

Lo único que hacemos aquí es declarar nuestro tipo de vértice, como hacíamos con DirectX 9. No, no es así de simple, hay un montón de cosas que explicar. Empezaré por la primera estructura, en la que aparece un nuevo "tipo básico" de DirectX llamado XMFLOAT4. Se trata de una de las muchas nuevas estructuras matemáticas añadidas a la API mediante la librería XNAMath. ¿Y de dónde sale XNA si lo que vamos a usar es DirectX11 a pelo? Pues se trata  de un intento por unificar parte de las APIs de ambas plataformas, aumentando la portabilidad entre PC y XBox; aunque aún no han marcado como deprecated los antiguos tipos, creo que es preferible ir migrando desde ya en lugar de hacer adaptaciones posteriores. Para añadir la librería a nuestro código hay que incluir el archivo "xnamath.h". En éste caso, XMFLOAT4 vendría a sustituir a D3DXVECTOR4.

La siguiente estructura es también muy similar a lo que estábamos acostumbrados, usamos el tipo D3D11_INPUT_ELEMENT_DESC en lugar de D3DVERTEXELEMENT9. Nótese cómo Microsoft ha optado por un nombrado más legible. El contenido de este nuevo tipo ha cambiado bastante, y para verlo mejor, observemos las diferencias:

typedef struct D3DVERTEXELEMENT9 {
  WORD Stream;
  WORD Offset;
  BYTE Type;
  BYTE Method;
  BYTE Usage;
  BYTE UsageIndex;
} D3DVERTEXELEMENT9, *LPD3DVERTEXELEMENT9;
typedef struct D3D11_INPUT_ELEMENT_DESC {
  LPCSTR                     SemanticName;
  UINT                       SemanticIndex;
  DXGI_FORMAT                Format;
  UINT                       InputSlot;
  UINT                       AlignedByteOffset;
  D3D11_INPUT_CLASSIFICATION InputSlotClass;
  UINT                       InstanceDataStepRate;
} D3D11_INPUT_ELEMENT_DESC;

Antes especificábamos el índice del stream que iba a contener vértices con tal elemento, el desfase de bytes respecto al inicio de la estructura, el tipo del elemento (vector de 3 floats, un entero, etc.), la interpretación del tesselator, la semántica del elemento en los shaders (elegido de un enumerado) y el índice del elemento para esa semántica. Ahora nos olvidamos de los streams y nos centramos únicamente en lo que respecta al formato de entrada a los shaders: determinamos la semántica del elemento en los shaders mediante una cadena, y su índice, por si hay más de un elemento con la misma; a continuación pasamos el formato del elemento, tal y como queremos que sea interpretado por los shaders (dense cuenta de que es el mismo enumerado de la API de DXGI que usamos anteriormente). El hueco de entrada (input slot) es un índice que determina por qué "puerto" del Input Assembler entrará la información de los vértices, de manera que, si ponemos los datos de posición (vectores de 3 componentes, por ejemplo) en un buffer de vértices y los datos de color (un entero) en otro buffer, podamos introducirlos por distintos slots y obtener como entrada al vertex shader una estructura con ambos datos ensamblados; ya veremos un ejemplo de esto en el futuro. Lo siguiente es especificar cuántos bytes de desfase hay entre el elemento actual, según su formato, respecto al precedente; lo normal aquí es usar la constante D3D11_APPEND_ALIGNED_ELEMENT que, como puede deducirse, pone el elemento justo después del anterior. Quisiera apuntar que el valor de este dato se calcula respecto al input slot al que pertenece el elemento, pudiendo tener 2 elementos con desfase cero si son los primeros de 2 slots distintos. Los 2 datos restantes está relacionados con el concepto de instanciación, disponible desde DirectX 10, al que dedicaré un post en su momento.

Alguien puede pensar que la declaración del tipo de vértice está incompleta, ¿dónde está la interfaz IDirect3DVertexDeclaration9? Ha dejado de existir, ahora lo que se crean son una especie de objetos intermediarios (layouts) que relacionan el array de la declaración con la firma de un shader determinado, como veremos más adelante. A partir de DirectX 10 ya no tenemos la fixed function pipeline, así que no es posible especificar el formato de un vértice mediante constantes D3DFVF, hay que seguir forzosamente ésta metodología. Otra cosa que falta a la declaración es el uso de la macro D3DDECL_END() como último elemento, que ya no es necesaria.

Paso 7: El cristal con que miramos

Ahora empieza la parte terrorífica de la historia, es hora de hablar de los… ¡shaders! Pero que nadie se achante, en realidad son como los dobermanns, acojonan al principio pero cuando los conoces pueden ser tus mejores amigos (vaya comparación de mierda :D ). El primer bicho con el que trataremos es la interfaz ID3DBlob, que encapsula un trozo de memoria de tamaño determinado, sin estructura definida. El uso que le daremos en esta ocasión será como recipiente de un shader compilado y como salida de los mensajes de error de la compilación. No voy a utilizar el Effects Framework todavía sino las funciones globales de que nos provee DirectX 11. Por tanto, para compilar un shader escrito en HLSL en un archivo de texto, llamaré a D3DXCompileShaderFromFile… ah no, que ahora se llama D3DX11CompileFromFile. Las diferencias entre una y otra función son ínfimas, quizá lo más destacable sea la posibilidad de realizar la compilación en un thread distinto mediante el uso de la interfaz ID3DX11ThreadPump, que no explicaré aquí, pero que según tengo entendido consume más de lo que ahorra. Para tener acceso a tal función hay que incluir la cabecera "D3DX11async.h".

UINT shaderFlags = D3D10_SHADER_DEBUG | D3D10_SHADER_SKIP_OPTIMIZATION | D3D10_SHADER_ENABLE_STRICTNESS;

ID3DBlob* pBlobVS = NULL;
ID3DBlob* pErrorBlobVS = NULL;
D3DX11CompileFromFile( L"MyEffect.fx", NULL, NULL, "vs_main", "vs_4_0", shaderFlags, NULL, NULL, &pBlobVS, &pErrorBlobVS, NULL );

ID3DBlob* pBlobPS = NULL;
ID3DBlob* pErrorBlobPS = NULL;
D3DX11CompileFromFile( L"MyEffect.fx", NULL, NULL, "ps_main", "ps_4_0", shaderFlags, NULL, NULL, &pBlobPS, &pErrorBlobPS, NULL );

Al igual que con las versiones anteriores, especificamos la ruta (relativa o absoluta) del archivo de texto, las macros del shader (que aquí no usamos), los trozos de shader a incluir (que tampoco necesitamos), el nombre de la función principal del shader y la versión del Shader Model para la cual se compilará. A continuación, se especifican los flags de compilación, valores constantes heredados de DirectX 10 (con prefijo D3D10_SHADER_) que se pueden combinar para modificar el comportamiento del compilador de shaders; aquí hemos usado, por ejemplo, D3D10_SHADER_DEBUG para generar información de depuración que sirva a un depurador, como el de PIX, para mostrar el código HLSL directamente, en lugar del código ensamblador; D3D10_SHADER_SKIP_OPTIMIZATION evita que el compilador realice optimizaciones, lo cual provocaría que el código HLSL visto en el depurador no concordara con el generado; finalmente, el flag D3D10_SHADER_ENABLE_STRICTNESS nos obligará a utilizar la sintaxis de la versión 4.0 y posteriores del Shader Model. Lo siguiente que se nos pide es una combinación de flags para efectos (no ponemos nada), un puntero a la interfaz del thread antes mencionada y variables de salida, de las que sólo cabe destacar la última, pHResult, que debe apuntar a una zona válida de memoria para almacenar el resultado de la operación llevada a cabo en el otro thread, siempre y cuando se haya especificado la susodicha interfaz. Otro NULL al bote.

Para poder insertar nuestros shaders en la pipeline, primero tenemos que encapsularlos en sendas interfaces ID3D11VertexShader y ID3D11PixelShader (antes llamadas IDirect3DVertexShader9 y IDirect3DPixelShader9, respectivamente). La única información que hay que pasar al dispositivo para crear los objetos es el punto de inicio del buffer que contiene el resultado de la compilación y su longitud. A continuación se asocian al contexto encargado de dibujar nuestro polígono.

ID3D11VertexShader* pVS = NULL;
pDevice->CreateVertexShader(pBlobVS->GetBufferPointer(), pBlobVS->GetBufferSize(), NULL, &pVS);
pImmediateContext->VSSetShader(pVS, NULL, 0);

ID3D11PixelShader* pPS = NULL;
pDevice->CreatePixelShader(pBlobPS->GetBufferPointer(), pBlobPS->GetBufferSize(), NULL, &pPS);
pImmediateContext->PSSetShader(pPS, NULL, 0);

Ya tenemos nuestros shaders cargados en memoria, compilados y preparados para ser ejecutados por el mecanismo de DirectX para representar nuestra geometría con el aspecto que más nos guste. Alto, ¿cómo sabe DirectX qué tipo de datos va a enviar al Input Assembler para que los shaders los procesen? ¿Cómo deben ser transformados aquéllos para que coincidan con los esperados?

Paso 8: Las gallinas que entran por las que van saliendo, o no

Como mencioné anteriormente, en DirectX 11 se usan unos intermediarios que actúan como adaptadores entre los shaders, el Input Assembler (por cierto, aunque ya lo expliraré, el Input Assembler es una de las fases (stage) de la pipeline, concretamente la de entrada) y los datos de entrada, que llegan en forma de paquetes de vértices. Estos adaptadores van encapsulados en interfaces ID3D11InputLayout y están sujetos a la firma de un shader y al formato de los datos de entrada, información que se transfiere al Input Assembler.

ID3D11InputLayout* pMyInputLayout = NULL;
pDevice->CreateInputLayout(arElements, sizeof(arElements) / sizeof(arElements[0]), pBlobVS->GetBufferPointer(), pBlobVS->GetBufferSize(), &pMyInputLayout);
pImmediateContext->IASetInputLayout(pMyInputLayout);

Tal y como puede verse claramente, se utilizan la declaración del tipo de datos de nuestro vértice y el resultado de la compilación del vertex shader (especificando sus puntos de inicio y su tamaño en memoria). Acto seguido se asocia el nuevo layout con el Input Assembler por medio del método IASetInputLayout (es obvio de dónde le viene el prefijo, veremos que han usado tal convención de nombres para todos los métodos relacionados directamente con ciertas fases de la pipeline).

Paso 9: Un pino, dos pinos, tres pinos… ¡coño un pinar!

Sólo resta meter datos en la tubería para que sean procesados por la maquinaria que acabamos de ajustar. Primero creamos una lista de 3 vértices cuyas coordenadas en el espacio proyectado sobre el viewport forman un triángulo (no vamos a aplicar transformaciones aún). Tal información irá empaquetada en un buffer cuya estructura debemos describir usando el tipo D3D11_BUFFER_DESC.

MyVertexType vertices[3];
vertices[0].position = XMFLOAT4(0.0f, 0.5f, 0.0f, 1.0f);
vertices[1].position = XMFLOAT4(0.5f, -0.5f, 0.0f, 1.0f);
vertices[2].position = XMFLOAT4(-0.5f, -0.5f, 0.0f, 1.0f);
vertices[0].color = vertices[1].color = vertices[2].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);

D3D11_BUFFER_DESC bufferDesc;
bufferDesc.Usage = D3D11_USAGE_DEFAULT;
bufferDesc.ByteWidth = sizeof( vertices );
bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bufferDesc.CPUAccessFlags = 0;
bufferDesc.MiscFlags = 0;

Antes de continuar, quisiera remarcar que el buffer que estamos definiendo es tratado por DirectX 11 como un recurso, al igual que las texturas (sus interfaces heredan de ID3D11Resource), por lo que la manera de crearlos y parametrizarlos es muy similar, basta observar cómo se repiten determinados campos en las estructuras D3D11_BUFFER_DESC, D3D11_TEXTURE1D_DESC, D3D11_TEXTURE2D_DESC y D3D11_TEXTURE3D_DESC. En DirectX 9 ocurría igual, aunque ahora se ha simplificado o unificado las opciones. Un ejemplo de esto era el parámetro de tipo D3DPOOL en el que especificábamos dónde alojar el recurso (memoria de sistema, de la tarjeta gráfica o administrado por DirectX), o la cantidad de tipos de uso, que ahora se ve reducida de 16 opciones a 4. También hay que mencionar la pérdida de las interfaces IDirect3DVertexBuffer9 e IDirect3DIndexBuffer9, cuyas responsabilidades son ahora cubiertas por ID3D11Buffer, según el flag de enlazado (bind flag).

Prosigamos. Rellenar la estructura es sencillo, como puede verse. Primero especificamos el uso (acceso de lectura / escritura por parte de la CPU y la GPU); luego el tamaño de nuestra lista de vértices; seguidamente el flag de enlazado, que en este caso indica que nuestro buffer actuará como un "vertex buffer"; no necesitamos acceder al recurso desde la CPU en esta ocasión, por lo que pasamos un cero, igual que para los flags especiales.

D3D11_SUBRESOURCE_DATA initialData;
initialData.pSysMem = vertices;
initialData.SysMemPitch = 0;
initialData.SysMemSlicePitch = 0;

ID3D11Buffer* pBuffer = NULL;
pDevice->CreateBuffer( &bufferDesc, &initialData, &pBuffer );

Para crear el buffer tenemos que "empaquetar" los datos en oootra esctructura (nótese mi jartura, con jota), D3D11_SUBRESOURCE_DATA, a la que pasamos un puntero a nuestra lista de vértices. Los otros 2 campos no son necesarios dado que no estamos tratando con una textura. De este modo, tenemos por un lado los datos y por otro su forma. Los usamos en el método CreateBuffer y hala, ya tenemos un dichoso buffer.

Como colofón, pasamos nuestro recién armado vertex buffer como entrada del Input Assembler:

UINT stride = sizeof( MyVertexType );
UINT offset = 0;
pImmediateContext->IASetVertexBuffers( 0, 1, &pBuffer, &stride, &offset );
pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

El método IASetVertexBuffers reemplaza al antiguo SetStreamSource, con el añadido de que se le pueden pasar varios vertex buffers en una sola llamada. La última sentencia especifica qué tipo de primitivas forman los vértices suministrados, es decir, si se trata de una lista de líneas (cogiendo los vértices de 2 en 2), o si es una tira (que es distinto de uns lista) de triángulos, etc. Aquí se envía una lista en la que hay un único triángulo.

El resultado de todo este trabajo lo podemos ver si llamamos al método Draw del immediate context. Existen varias versiones de este método según el proceso que queramos seguir a la hora de dibujar. Por ejemplo, si estuviéramos usando índices llamaríamos a DrawIndexed, y en caso de usar instanciación invocaríamos a DrawInstanced. Hablaré de todos ellos en los próximos posts. Como es obvio, tales métodos son evoluciones de sus casi homónimos de la interfaz del dispositivo de DirectX 9.

pImmediateContext->Draw( 3, 0 );

Con esto expresamos que queremos dibujar 3 vértices, empezando por el número cero. Y éste es el resultado:

Sumario de Portabilidad

DirectX 9 DirectX 11 Comentario
D3DVIEWPORT9 D3D11_VIEWPORT  
D3DXFLOAT4 XMFLOAT4  
D3DVERTEXELEMENT9 D3D11_INPUT_ELEMENT_DESC  
IDirect3DVertexDeclaration9   No hay análogo.
Constantes D3DFVF   No hay análogo.
D3DDECL_END()   No hay análogo.
D3DXCompileShaderFromFile D3DX11CompileFromFile  
IDirect3DVertexBuffer9 ID3D11Buffer Bind Flag = D3D11_BIND_VERTEX_BUFFER
IDirect3DIndexBuffer9 ID3D11Buffer Bind Flag = D3D11_BIND_INDEX_BUFFER
IDirect3DDevice9::CreateVertexBuffer ID3D11Device::CreateBuffer  
IDirect3DDevice9::CreateIndexBuffer ID3D11Device::CreateBuffer  
IDirect3DVertexShader9 ID3D11VertexShader  
IDirect3DPixelShader9 ID3D11PixelShader  
IDirect3DDevice9::SetStreamSource ID3D11DeviceContext::IASetVertexBuffers  
IDirect3DDevice9::DrawPrimitive ID3D11DeviceContext::Draw  

 

Descargas

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

EndlessGameTriangle.zip (23 Kb)
 

Etiquetas: , ,

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: , ,

Inicializando Direct3D – Parte 1/2

Publicado el Martes 20 julio 2010 en DirectX por Thund

¡Por fin, con todos ustedes… el más esperado, el desconocido, el novedoso… DirectX 11! Estoy de acuerdo, vaya chorrada de presentación, pero tenía que hacerlo, lo juro. El presente post y muchos de los sucesivos estarán centrados en Direct3D y DXGI, con pinceladas de DirectWrite o Direct2D, y en ellos trataré de explicar cómo están estructurados, cómo interactúan y cómo se utilizan para distintos propósitos, intercalando teoría y práctica para hacerlo más llevadero y comprensible. Para no complicarlo mucho, el código presentado será el básico e indispensable (en el próximo post ya le añadiré comprobaciones para hacerlo más robusto). Empecemos pues con un corto recorrido alrededor de los principales componentes de Direct3D y los cambios que ésta API ha sufrido respecto a DirectX 9. Como muchos recordaréis, lo primero que se hacía, a parte de la creación de la ventana, era llamar a la función Direct3DCreate9 a la que pasábamos la versión del SDK para obtener un puntero a una interfaz IDirect3D9. Esta última se encargaba de crear el manejador del dispositivo gráfico (IDirect3DDevice9) y la swap chain, y nos surtía de una serie de funciones para comprobar las posibilidades que el hardware ofrecía, de cara a dicha creación. Pues bien, este diseño se ha ido al carajo. Ya no existe una interfaz de entrada a la librería y toda la responsabilidad de comprobación de hardware ha sido desplazada a la API de DXGI. La swap chain ha dejado de estar medio oculta al usuario (tenías que ir específicamente a buscarla para darte cuenta de que estaba ahí) y pasa a primer plano bajo control de interfaces IDXGISwapChain. Como se puede comprobar por el prefijo, también ha pasado a dominio de DXGI. La interfaz del dispositivo (ahora ID3D11Device) se genera gracias a la función global D3D11CreateDevice y ya no se ocupa del envío de instrucciones de dibujo, o al menos no directamente, sino que ha sido dividida en 2 partes: el dispositivo y el contexto del dispositivo (ID3D11DeviceContext); mientras que el primero representa el adaptador en sí y actúa como fábrica de ciertos objetos, el segundo maneja las órdenes de renderizado. Como colofón, hace falta crear, al menos, un render target que apunte al back buffer de nuestra swap chain, y que usaremos para indicarle al device context dónde tiene que dibujar. Sí, Microsoft ha complicado un pelín las cosas. Vamos a verlo en mayor detalle, que así parece muy enrevesado.

 

DirectX Graphics Infrastructure

Nació con DirectX 10 para, según Microsoft, agrupar aquellas porciones de la API gráfica encargadas de realizar operaciones a bajo nivel en una librería independiente. Tales tareas son, por ejemplo, la enumeración de los dispositivos gráficos, el envío de los frames renderizados a los dispositivos de salida (presentación), manejo de transiciones de pantalla completa, etc. Ciertamente, DXGI ha cambiado poco desde que se lanzó, lo único que han hecho ha sido añadir funciones y unas cuantas chorradas* más con la versión 1.1, que aquí ni veremos ni usaremos. Identificaremos fácilmente estas extensiones, ya que los chicos de Redmond han tomado la genial decisión de diseño de añadir un "1" como sufijo, y deberemos tener cuidado de no mezclarla con la versión 1.0, esto es, no se deben usar interfaces o funciones de una y a continuación invocar las de la otra, de lo contrario vendrá Steve Ballmer y os gritará "Developers!" en un oído. Sí bueno, es la típica historia de terror que se cuenta a los niños cuando no se tiene ni puta idea de qué es lo que pasa, como el coco o el efecto invernadero, y con eso hay que conformarse :) . A continuación el conocido esquema sobre DXGI, por cortesía (o no) de Microsoft:

[Diagrama DXGI]

ID3D11Device

Realiza las mismas funciones que su antecesor, menos las relativas al renderizado, como Clear, Draw, Begin, End… Básicamente, es una fábrica de objetos o recursos asociados al dispositivo. Pueden generarse recursos compartidos entre varios dispositivos, pero sólo los podrá usar aquél que los creó. Echando un vistazo a su interfaz os daréis cuenta de que ya no están disponibles los métodos para la creación de vertex buffers e index buffers; de hecho, sí que están, lo que ocurre es que se han unificado los buffers en una única interfaz llamada ID3D11Buffer, que encapsula un objeto cuya funcionalidad puede configurarse en el momento de su creación, de la que se encarga el método CreateBuffer, para que se comporte como index, vertex o constant buffer. Un dispositivo puede dividirse internamente en 2 capas: la capa principal o núcleo (Core Layer), que existe siempre por defecto, y la capa de depuración (Debug Layer), que puede activarse mediante el flag D3D11_CREATE_DEVICE_DEBUG, cuando creamos el manejador del dispositivo. Dependiendo de cómo tengamos configurado el panel de control de DirectX, la capa de depuración nos mostrará una serie de mensajes muy últiles en la ventana de Output de Visual Studio. Cabe destacar que la interfaz ID3D11Device es free-threaded (o thread-safe, como más os guste), lo que significa que se puede llamar a sus métodos desde cualquier hilo simultáneamente (el número de concurrencias dependerá del driver). Al igual que en anteriores versiones, podemos crear un dispositivo de referencia que emulará mediante software todas las características de DirectX. Adicionalmente, existe otro tipo de dispositivo llamado WARP (Windows Advanced Rasterization Platform), que también implementa mediante software (con mucho mejor rendimiento, según tengo entendido) las capacidades de Direct3D 11. Para ver las limitaciones de ambos emuladores echad un ojo a la documentación.

ID3D11DeviceContext

La interfaz del dispositivo antes mencionada ha sido dividida en 2 partes, de manera que la responsabilidad del renderizado ahora recae en los llamados "contextos del dispositivo". Los contextos son creados por el propio dispositivo y pueden ser de 2 tipos: inmediatos y postergados (es mi traducción de deferred). El concepto de "contexto" se refiere al modo de tratar las órdenes de renderizado y cambios de estado que se envían al dispositivo. Mientras que en un contexto inmediato los comandos son transmitidos directamente al dispositivo, en un contexto postergado se genera una cola de comandos que serán ejecutados más tarde utilizando el contexto inmediato. Esto permite utilizar una arquitectura multihilo para renderizar en paralelo aunque, al final, acabe realizándose en serie. Sólo puede existir un contexto de tipo inmediato por dispositivo, e infinitos contextos postergados. Para saber a qué tipo pertenece un determinado contexto, podemos usar el método GetType. Al contrario que la interfaz ID3D11Device, ID3D11DeviceContext no es thread-safe, sólo puede usarse cada instancia desde un único hilo simultáneamente.

IDXGISwapChain

Anteriormente, para acceder a la cadena de intercambio de buffers de salida (swap chain en adelante), teníamos que utilizar el método GetSwapChain de la interfaz IDirect3DDevice9, obteniendo así un puntero a la interfaz IDirect3DSwapChain. Su existencia pasaba inadvertida, aunque la utilizáramos sin saberlo al suministrar la estructura D3DPRESENT_PARAMETERS (cuyo equivalente ahora es DXGI_SWAP_CHAIN_DESC) o al llamar al método Present. Con el diseño actual, la swap chain pasa a un discreto primer plano y no sólo eso sino que, como mencioné en varias ocasiones, las interfaces relacionadas con la misma han sido movidas a la librería de DXGI.

ID3D11RenderTargetView

Antes de explicar el significado de ésta interfaz en concreto, creo que es necesario dar a conocer algunos detalles acerca de la estructura de DirectX 11. Existen los llamados "recursos", que no son más que zonas de memoria con datos que son accesibles desde la pipeline de Direct3D. Tales recursos, encapsulados en derivadas de ID3D11Resource, son generados por la interfaz del dispositivo y normalmente contienen datos "en bruto", en forma de buffers (ID3D11Buffer), o texels, formando texturas (ID3D11Texture1D, ID3D11Texture2D ó ID3D11Texture3D); hay varios tipos de recurso más o, mejor dicho, subtipos, que han sido añadidos con la versión 5.0 de los shaders, tales como StructuredBuffer, que no describiré hasta que lo necesitemos para algo en el futuro. La cuestión es que, para manipular tales datos de memoria, necesitamos una "vista", que actúa como canal de acceso; en función del tipo de recurso, tendremos que elegir un tipo de vista u otro, esto es, una u otra interfaz derivada de ID3D11View. ¿Que por qué me pongo a hablar de esto aquí en vez de crear una sección de introducción para los recursos, otra para las vistas, etc.? Pues porque para fusilar y traducir la documentación de referencia ya tenéis la MSDN y el infalible traductor online de Microsoft. Volviendo a lo que nos ocupa, uno de los tipos de vista que nos permiten dibujar sobre una textura (nuestro back buffer es precisamente eso, una textura 2D) es el representado por ID3D11RenderTargetView. Para crearla hay que usar el método CreateRenderTargetView del dispositivo, al cual hay que suministrar la textura (nuestro back buffer, por ejemplo, obtenido mediante el método GetBuffer de la interfaz de la swap chain) y, opcionalmente, una estructura de configuración. Las vistas son objetos pasivos, tan sólo tienen un método o propiedad que nos devuelve sus descripciones. Veremos más tipos de vista en breve, cuando hable del depth y el stencil buffer.

Sé que el principio del post prometía sangre, pero dado que soy un cenutrio incapaz de condensar tanta información para hacer posts agradables y ligeros, me veo obligado a partirlo en 2. No tardaré mucho en publicar la segunda parte, que es la que contiene el ejemplo de código, que nadie se impaciente.

 

*Para los puristas, cuando digo "chorradas" quiero decir "funcionalidad de uso poco frecuente o de propósito muy específico", aunque suponga una puta revolución tecnológica.

Etiquetas: ,

Empecemos por el principio…

Publicado el Martes 29 junio 2010 en DirectX por Thund

Puede que tengáis las mismas ganas de hincarle el diente a DirectX 11 que yo, pero antes de meternos en harina vamos a necesitar un entorno sobre el que trabajar. Sé que sois todos muy listos pero nunca viene mal tener recogidos en un sitio los pasos a seguir para instalar todo aquello que necesitamos, así evitamos perder el tiempo buscando la próxima vez. Dado que vamos a trabajar con DX11, presupondré que estáis leyendo esto desde un Windows 7.

Microsoft Visual Studio 2010

La razón por la que elijo esta versión responde únicamente a su novedad, por ir acostumbrándome al nuevo IDE. En éste momento alguien saltará diciendo: "¡Pero gañán, si es prácticamente igual!". Totalmente de acuerdo, salvo los colorines no se han roto demasiado la cabeza. Que sí, que internamente lo han reconstruido usando WPF (Windows Presentation Foundation) y MEF (Managed Extensibility Framework) pero, tristemente, en lo único que lo he notado ha sido en lo borroso que se ven las letras del editor de texto… es un problema conocido y que aún no tiene solución oficial (aunque Microsoft lo tenga como fixed, es una trola que sólo se tragan los bizcos), WPF renderiza mal las letras pequeñas, y si eres de los que usa fondo negro, como yo, tanto peor. Más o menos uno se acostumbra (a base de ganar dioptrías), lo he dejado en la fuente por defecto, Consolas, aunque a le he aumentado el tamaño a 11. El resto de la interfaz, según opiniones varias, es como VS2008 + Resharper, si bien los creadores del plug-in se defienden del plagio como pueden. De cualquier modo, a los programadores de C++ eso nos la trae floja. Tanto nos la suda que voy a pasar de enumerar las nuevas virtudes de VS, primero, porque no es la temática ni del blog ni del post y, segundo, porque salvando alguna leve mejora del IntelliSense (muy leeeve muy leeve….), han mejorado bastante poco el apoyo al desarrollo de C++ nativo; sinceramente, el subrayado rojo molesta más que ayuda. Tanto que anunciaban que iban a poner el soporte a C++ a la altura del de C# o VB.Net… falacias. Al menos espero que hayan afinado, aún más, la optimización de rendimiento del compilador (que ahora es MSBuild). Es digno de mención que VS2010 viene ya con soporte para el nuevo estándar de C++0x (curioso, cuando aún no ha sido publicado). Sí, en conclusión, elijo VS2010 por ser más guay.

Vamos a dejar las pataletas a un lado y pongámonos a instalar la versión Express, que como ya sabéis es gratuita. A cambio, como es costumbre, recortan una serie de características: en el caso de VC++ Express, lo distribuyen sin las librerías MFC ni ATL, que no vamos a usar para nada, y capan alguna capacidad relacionada con la compilación para 64 bits, que tampoco necesitamos. No obstante, si alguien se quiere bajar comprar la versión Full puede estar tranquilo de que cualquier código de éste blog le va a funcionar igual que en la versión reducida.

Paso a paso para torpes:

Bajar Microsoft Visual Studio 2010 Express

En la lista desplegable (alias combo), selecciona English. Yo lo prefiero en inglés y los tutoriales estarán basados en tal idioma.

Esto bajará un instalador online llamado vc_web.exe, de 3'2 Mb. También puedes elegir la opción de abajo del todo, que es una imagen ISO que contiene todas las versiones Express de los lenguajes soportados por VS; el archivo se llama VS2010Express1.ISO, ocupa 694 Mb y a mí me tardó 15 minutos en bajarse, a 800 Kb/s. Luego sólo tendrás que montarlo con Daemon, por ejemplo, y seleccionar VC++.

Cuando ejecutes el instalador online, desmarca todos los checkboxes que encuentres y pulsa Install.

La instalación requerirá 954 Mb de disco duro y necesitará descargar unos 75 Mb. En aproximadamente 5 minutos debería terminar.

Comprueba que se ha instalado todo correctamente.

Al tiempo te saltará una ventana pidiendo que registres el producto. Puedes pasar de ella si no te molesta o puedes registrarlo, que no cuesta nada. Si quieres registrarlo (desconozco ahora mismo qué ventaja aporta ni me he preocupado en investigarlo), pulsa "Obtain a registration key online", lo que te llevará a una web de Microsoft con un formulario. Por cierto, si no ves la ventana de aviso, puedes acceder a ella usando el menú "Help"–>"Register Product". Pon lo que te de la gana en los campos del formulario, incluso puedes inventarte el email ya que te dan el código en la misma web y, a parte, envían una copia al correo. Venga va, que sé que más de uno es vago de cojones, aquí tenéis mi clave:

6VPJ7-H3CXH-HBTPT-X4T74-3YVY7

Microsoft DirectX 11 SDK

Eje central del blog, es la última versión de las librerías que nos permiten utilizar el HAL (Hardware Abstraction Layer) de Windows para comunicarnos con los drivers y manejar así ciertos dispositivos hardware. No voy a ponerme aquí a explicar en detalle qué es DirectX, para eso ya hay muchos sitios. En su lugar, haré un breve resumen de la nueva estructura de la versión 11 con respecto a la 9 pues considero que hay mucha más gente interesada en portar su código desde la versión 9.0c que desde la 10.1.

  • Direct3D: Se mantiene la rama principal para el control de los dispositivos gráficos que, desde Vista, se realiza sobre WDDM 1.1 (Windows Display Driver Model), nueva arquitectura de los drivers para tarjetas gráficas, en teoría más robusta, más rápida y que provee nuevas funcionalidades como la posibilidad de compartir superficies (texturas) entre threads, virtualización de memoria de video (paginación en la RAM del sistema) o encolar tareas de la GPU. A nivel de interfaces de la librería, nos encontramos con que ya no hay más llamadas a la instancia del dispositivo para cambiar estados de la fixed pipeline, que ha sido totalmente desechada y traducida a shaders 2.0 que tendremos que programar. Otra novedad significativa es la separación de las tareas cuya evolución se intuye mucho más lenta que el resto, como es la enumeración de dispositivos gráficos y el manejo de swap chains, en una nueva rama de interfaces llamada DXGI (DirectX Graphics Infrastructure). Ni que decir tiene que los nombres de las funciones de ayuda, las interfaces, las estructuras y demás han cambiado; algunos son irreconocibles mientras que otros simplemente han reemplazado los prefijos D3DX- e IDirect3D- por D3D11- (funciones), ID3D11- (interfaces) y D3D11_- (estructuras y enumerados). Hay muchas cosas más (shaders 5.0, Geometry Shader, desaparición de las Device Caps, etc.) pero ya iremos descubriendo las sorpresas que Microsoft ha preparado conforme vaya publicando entradas en el blog, que si no os empacháis.
  • DXGI: Como ya he mencionado, este nuevo grupo de interfaces encapsulan algunas de las tareas que antes formaban parte de IDirect3DDevice9 y que se estima que van a cambiar a un ritmo mucho más lento que el resto de características de Direct3D. Por ejemplo, a la hora de obtener el identificador de un dispositivo gráfico ya no usaremos el método IDirect3DDevice9::GetAdapterIdentifier, sino IDXGIDevice::GetAdapter; de igual modo, no usaremos el método IDirect3DDevice9::GetAdapterCount junto con el anterior para obtener la lista de dispositivos, directamente podremos llamar a IDXGIDevice::EnumAdapters. La administración de swap chains también ha sido delegada en DXGI y existen un gran surtido de métodos e interfaces para llevarla a cabo con mayor facilidad.
  • Direct2D: Vino con Windows 7, y lo hizo para quedarse. Podemos ir diciendo adiós con la manita a DirectDraw junto a la GDI y GDI+ (siguen existiendo, pero no tendrás que usarlos más). Como su nombre indica, engloba toda la funcionalidad relacionada con el dibujo bidimensional y es el responsable de todos los píxeles que ves cuando usas Windows 7 (para que funcione con Vista hay que instalar actualizaciones). Unifica las APIs antes mencionadas y ofrece un conjunto de interfaces cuya usabilidad es notablemente superior. Es realmente sencillo dibujar una figura mediante el uso de Brushes (interfaces derivadas de ID2D1Brush), que determinan la metodología de coloración y otros aspectos visuales como, por ejemplo, si se usará un degradado lineal de color negro a amarillo para rellenar una superficie; Geometries (interfaces derivadas de ID2D1Geometry), que definen la forma del polígono a representar, como un rectángulo o una elipse; Bitmaps (interfaz ID2D1Bitmap)… no, no pongo los puntos suspensivos a modo de elipsis, los pongo a modo de suspiro debido a que llegados a este punto se acabó la simplicidad. No diré que sea mucho más engorroso que usando DirectDraw, pero tener que recurrir a otro framework, WIC (Windows Imaging Component) para más señas, para cargar la imagen y luego traspasarla al sistema de Direct2D me parece cuanto menos insuficiente, no porque sea malo separar responsabilidades (la idea tras de WIC no es mala ni mucho menos), sino porque podrían haberse currado algún wrapper o función de ayuda para tal efecto, ya que estamos; aun así, no os agobiéis, existe un ejemplo bastante claro en el archivo de ayuda del SDK (busca "How to Load a Bitmap from a File", comillas incluidas). Por cierto, vaya coñazo es Microsoft con tanto puto acrónimo. Por último, acabando con éste apartado, se puede dibujar texto con Direct2D, aunque las posibilidades que ofrece son bastante reducidas respecto a DirectWrite.
  • DirectX Media: Es un grupo de APIs que facilitan las labores relacionadas con contenidos multimedia. Está compuesta por DirectAnimation, para animación web (aviso que la información de esta es jodidamente escasa); DirectShow, para manejo de video y streaming, de la cual ya disponíamos en DirectX 8, aunque Microsoft lo separó del SDK de DirectX y mareó un poco al personal situándolo en varias librerías, hasta hoy; DirectTransform, otra API orientada al desarrollo web que nos surte de funciones para crear "animaciones" mediante transiciones entre frames (imágenes estáticas), añadiendo ciertos efectos; existe alguna más pero creo que carece de interés. La tendencia respecto a DirectShow ha sido, claramente, trasladarlo a un contexto de funcionalidad de sistema operativo, separándolo del tronco principal dedicado al desarrollo de videojuegos. Lamento decir que, aunque la plataforma DirectX Media siga vigente, será pronto sustituida por la MF (Media Foundation), aparecida junto a Windows 7. Dado que eso es otro mundo, inmenso e incierto como todos los que abre Microsoft cuando sus chicos se aburren y hay pasta de sobra, no voy a meter más la cabeza en él.
  • DirectInput: Seguimos disponiendo de éste conjunto de interfaces, que nos permiten consultar el estado de ciertos periféricos, como el teclado, el ratón, gamepads, joysticks… o enviarles señales (force feedback) a, por ejemplo, un gamepad con vibración. No ha evolucionado nada desde DirectX 8, así que poco se puede añadir. Sí cabe mencionar la postura de Microsoft respecto a su uso pues, dado que hace años que no dan soporte para esta API, aconsejan valerse del bucle de mensajes de Windows para manejar las entradas de los dispositivos, haciendo hincapié, además, en que utilicemos la librería XInput (introducida en DirectX 9) en el caso de que queramos usar mandos de XBox. Lo que les costará mantener y mejorar DirectInput, que son un par de estructuras unas pocas funciones… algo tendrán planeado.
  • DirectPlay: Creo que ésta es la rama que más desconozco de DirectX, será porque soy un manta con las telecomunicaciones y eso ha hecho que me interese bastante poco. Todavía disponemos de ella para llevar a cabo comunicaciones de red entre distintos PCs. Al igual que ocurrió con DirectInput, Microsoft le dio la patada hace mucho, por lo que se aconseja usar Winsock en su lugar. Leyendo las opiniones que circulan por Internet, parece ser que no era mucho más sencillo implementar el sistema de red con DirectPlay que con Winsock a pelo, así que yo no le dedicaría mucho a esta API.
  • DirectSound: Fue actualizada por última vez en la versión 8 y hoy día sigue estando disponible. Actúa como capa de bajo nivel entre nuestro software y el hardware de sonido, facilitando la reproducción de archivos de audio. Desgraciadamente (para algunos), Windows Vista acometió una serie de reformas en el apartado del control de audio con el fin de unificar las interfaces y capacidades del hardware de los distintos proveedores, dando lugar a la UAA (Universal Audio Architecture), reemplazando parcialmente el WDM (Windows Driver Model). De esta forma, se rompía la conexión directa de DirectSound con los drivers, necesitando ahora emulación por software; si bien el impacto de rendimiento es imperceptible, algunas características del subconjunto DirectSound3D quedaron anuladas. Los proveedores de controladores suministraron entonces ciertas herramientas para interceptar las llamadas a esta API y "reactivar" la funcionalidad perdida. En definitiva, en vista de la nueva plataforma XAudio por la que está apostando Microsoft, podemos dar a DirectSound por muerta, no malgastéis vuestro tiempo.
  • DirectMusic: Sobra un poco hablar de esta rama ya que, como algunos sabréis, funciona sobre el ya fallecido DirectSound. Al igual que esta última, sigue existiendo en DirectX 11, facilitando un poco la labor de orquestar la reproducción de audio pregrabado y la ejecución de piezas musicales mediante un sintetizador (véanse los casi desaparecidos MIDIs). En este momento desconozco si existe un reemplazo inmediato, puede que XAudio provea de alguna funcionalidad parecida.
  • XAudio2: Aparece con motivo de los esfuerzos de Microsoft por unificar las plataformas de Windows con XBox, en este caso, en el ámbito del sonido. No, el 2 no es una errata, he decidido hablar de la segunda versión de la API puesto que la primera estaba dedicada a manejar el audio de la consola, únicamente, mientras que XAudio2 es "multiplataforma" entre PC, bajo Windows XP o superior, y XBox 360. Sustituye, como dije anteriormente, a DirectSound y forma parte de (o es usado por) la plataforma XACT3 (Cross-platform Audio Creation Tool), un conjunto de APIs de alto nivel y herramientas (aplicaciones) para la generación de contenidos de audio. Esta librería utiliza lo que denomina como voices (eso, voces) para trabajar con sonidos pregrabados, ofreciendo funciones para su manipulación y permitiendo crear grafos de remezclas para obtener salidas de audio nuevas. Existe un complemento llamado X3DAudio (como hubo un DirectSound3D) para aplicación de efectos de posicionamiento tridimensional (variación del volumen de cada altavoz de un sistema estéreo, 5.1, 7.1, etc. en función de unas coordenadas [X, Y, Z]), efecto Doppler y eco. Como pasará bastante tiempo antes de que le meta mano a esta API en el blog, os pongo un artículo de Gamasutra muy revelador, por si os interesa.
  • DirectCompute: La arquitectura de DirectX ha dado un paso significativo hacia delante (acompañada, como suele ser habitual, por la del hardware), abriendo definitivamente la puerta a la computación de propósito general usando la VRAM y la GPU de nuetras tarjetas gráficas. Hasta ahora, podíamos delegar en la GPU una serie de trabajos concretos, como modificar el color de un píxel (vía Pixel Shaders, también conocidos como Fragment Shaders), generar un polígono (mediante Geometry Shaders, incorporados en DirectX 10) o desplazar un vértice (valiéndonos de los Vertex Shaders), pero, como ya digo, con limitaciones propias de la función para la que fueron concebidos. Y llamo limitación a tener que renderizar un vértice para que se disparen los shaders que necesites. Con DirectCompute, explicándolo burdamente, reservas un espacio de memoria visible desde el contexto de los shaders, creas una serie de threads y los "activas", de manera que tu shader realiza las operaciones que hayas programado con HLSL o ensamblador y guarda los resultados en los espacios que reservaste. Ya explicaré esto en más detalle llegado el momento, porque tiene tela… Parece ser que la trayectoria de la computación se ha desviado un poco, desechando la idea de la expansión de las CPUs multinúcleo y abrazando el dogma del proceso en paralelo entre CPU y GPU; según AMD, mientras las CPUs actuales alcanzan velocidades de 108'8 GigaFLOPS (Floating-point Operations Per Second), las GPUs están superando los 1.36 TeraFLOPS (11 veces superior), así que está clara la tendencia.
  • DirectWrite: Ésta sección de DirectX nos provee de un abanico de funcionalidades para el dibujo de texto en pantalla. Llegó a la par que Direct2D y con el mismo propósito de establecerse por encima de todas las librerías del sistema dedicadas al manejo de texto. Nos despedimos pues del uso del GDI y de la interfaz ID3DXFont de Direct3D. Ahora la respuesta a la gran pregunta ( [FreakMode] tranquilos, trataré de responder con algo más que un par de cifras [/FreakMode] ): si Direct2D ya pinta letras, ¿para qué carajo queremos DirectWrite? Sencillo y complejo a la vez. Sencillo porque, básicamente, cuando trabajan juntos, Direct2D se dedica a dibujar y DirectWrite le indica qué dibujar, amén de otros "servicios" que ofrece, como el acceso a las fuentes (entiéndase tipografías) del sistema operativo y sus características. La explicación compleja, según Microsoft, justifica la decisión de escindir DirectWrite por motivos de facilidad de adopción por parte de las aplicaciones ya existentes que hacen uso del GDI para representar sus textos; es más sencillo lidiar sólo con la parte de proceso de textos que sustituir todo el sistema gráfico 2D que, posiblemente, no se necesite o no se pueda cambiar. Esto implica que, según he entendido (no he hecho experimentos aún, todo llegará), es posible dibujar las letras como mejor te guste, usando Render Targets (interfaces que derivan de ID2D1RenderTarget) o implementando la interfaz IDWriteTextRenderer para traducir lo que llegue a sus métodos de callback a instrucciones del GDI, por ejemplo.

Joder, y yo que pensaba no alargarme mucho… Bueno, lo siguiente es rapidito, vamos a instalar el SDK de DirectX 11 (que incluye además el 9c, el 9Ex y el 10.1) y vamos a dejarlo listo para trabajar.

Paso a paso para torpes:

Bajar SDK de DirectX 11 de Junio de 2010

Pulsa en Download, que te llevará a una página de confirmación. Allí pulsa Start Download.

Se descargará un archivo de 571'7 Mb llamado DXSDK_Jun10.exe. A mí me tardó 10 minutos, a 800 Kb/s.

Ejecuta el instalador. Requerirá 1251 Mb de espacio en disco.

Desmarca la casilla de participación si quieres y cambia el estado de aquellos elementos de la lista que estén marcados como no disponibles (sólo debería haber uno así), o sea, instálalo todo.

Después de unos 4 minutos aproximadamente debería haber terminado.

Pero la cosa no acaba aquí. Si vas al VS 2010 para comprobar que el instalador haya añadido las correspondientes rutas del SDK a la lista de directorios de VC++, te darás cuenta de que NO EXISTE tal lista. Bueno, realmente sí que existe, pero comprobarás que la han movido. En efecto, ahora no hay una lista a nivel de usuario para todos los proyectos, sino que cada proyecto tiene su propia lista por defecto, en forma de propiedad. Si quieres hacer esta comprobación no tienes más que crear un proyecto Win32 cualquiera e irte a las propiedades del mismo, deberían estar las siguientes entradas en la página de VC++ Directories:

  • Include Directories: C:\Program Files\Microsoft DirectX SDK (June 2010)\Include
  • Library Directories: C:\Program Files\Microsoft DirectX SDK (June 2010)\Lib\x86

Si no estuvieran, inclúyelas cada vez que crees un proyecto. O mejor aún, ve al menú View –> Property Manager; se abrirá en la barra lateral un árbol de hojas de propiedades. Abre la que se llama "Microsoft.Cpp.Win32.User" y modifica todo aquello que necesites que esté por defecto en cada nuevo proyecto.

Pues esto es todo, iba a meter un mini-proyecto Win32 para dejar lista la parte de código que no tiene que ver con DirectX directamente y olvidarnos ya de la infraestructura, pero como me ha salido tan largo este post, lo dejaré para el siguiente. Espero ser más escueto en próximas publicaciones y que toda esta información sea útil para más de un frikazo tecnológico.

PD: He puesto todos los enlaces a páginas en inglés dado que odio las traducciones automáticas, que suelen a inducir a errores, y porque las fuentes en inglés suelen tener contenidos más amplios y más fiables. Espero que no sea un gran impedimento para nadie.

Etiquetas: , , ,