Compartir a través de


Trabajar con recursos de dispositivos DirectX

Comprenda el rol de la Infraestructura de gráficos de Microsoft DirectX (DXGI) en su juego DirectX de la Tienda Windows. La infraestructura DXGI es un conjunto de API que se usa para configurar y administrar recursos de adaptadores de gráficos y gráficos de bajo nivel. Sin esta, no sería posible dibujar los gráficos de sus juegos en una ventana.

Piense en DXGI de esta forma: para acceder directamente a la GPU y administrar sus recursos, debe tener una forma de describirlo en la aplicación. La información más importante que necesita sobre la GPU es el lugar donde dibujar los píxeles para poder enviar esos píxeles a la pantalla. Esto se suele llamar "búfer de reserva", que es una ubicación en la memoria GPU donde puede dibujar los píxeles y, a continuación, hacer que se "volteen" o se "intercambien" y se envíen a la pantalla con una señal de actualización. DXGI le permite adquirir esa ubicación y los medios para usar ese búfer (lo que se denomina cadena de intercambio, porque es una cadena de búferes intercambiables, que permite diversas estrategias de almacenamiento en búfer).

Para ello, necesita acceso de escritura en la cadena de intercambio y un identificador para la ventana que mostrará el búfer de reserva actual para la cadena de intercambio. También debe conectar ambos para asegurarse de que el sistema operativo actualice la ventana con el contenido del búfer de reserva cuando lo solicite.

El proceso general para dibujar en la pantalla es el siguiente:

  • Obtenga un elemento CoreWindow para su aplicación.
  • Obtenga una interfaz para el contexto y el dispositivo Direct3D.
  • Cree la cadena de intercambio para mostrar la imagen representada en el elemento CoreWindow.
  • Cree un destino de representación para dibujar y rellenarlo con píxeles.
  • Presente la cadena de intercambio.

Creación de una ventana para la aplicación

Lo primero que debemos hacer es crear una ventana. En primer lugar, rellene una instancia de WNDCLASS para crear una clase window y, a continuación, regístrela con RegisterClass. La clase window contiene las propiedades esenciales de la ventana, incluido el icono que usa, la función de procesamiento de mensajes estáticos (más información más adelante) y un nombre único para la clase 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);
}

A continuación, se crea la ventana. También es necesario proporcionar información del tamaño de la ventana y el nombre de la clase window que acabamos de crear. Al llamar a CreateWindow, se le devuelve un puntero opaco a la ventana que se denomina HWND; deberá conservar el puntero HWND y usarlo siempre que tenga que hacer referencia a la ventana (incluso destruirla o recrearla) y, sobre todo, al crear la cadena de intercambio DXGI que usa para dibujar en la ventana.

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

El modelo de aplicación de escritorio de Windows incluye un enlace al bucle de mensajes de Windows. Deberá basar el bucle de programa principal fuera de este enlace; para ello, escriba una función "StaticWindowProc" que procese eventos basados en ventanas. Esta debe ser una función estática, ya que Windows la llamará fuera del contexto de cualquier instancia de clase. A continuación, se muestra un ejemplo muy sencillo de una función de procesamiento de mensajes estáticos.

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

En este sencillo ejemplo solo se comprueban las condiciones de salida del programa: WM_CLOSE, que se envía cuando se solicita cerrar la ventana, y WM_DESTROY, que se envía después de quitar la ventana de la pantalla. Una aplicación de producción completa también debe controlar otros eventos basados en ventanas; para obtener la lista completa de esos eventos, consulte Notificaciones de ventana.

El bucle de programa principal en sí debe reconocer los mensajes de Windows, para lo que permite que Windows pueda ejecutar el procesamiento de mensajes estáticos. Ayude a que el programa se ejecute de forma eficaz al bifurcar el comportamiento: cada iteración debe elegir procesar nuevos mensajes de Windows si están disponibles y, si no hay ningún mensaje en la cola, debe representar un nuevo fotograma. Este es un ejemplo muy sencillo:

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

Obtención de una interfaz para el contexto y el dispositivo Direct3D

El primer paso para usar Direct3D es adquirir una interfaz para el hardware direct3D (la GPU), que se representa como instancias de ID3D11Device y ID3D11DeviceContext. La primera es una representación virtual de los recursos de la GPU y la segunda es una abstracción independiente del dispositivo del proceso y la canalización de representación. Esta es una forma fácil de verlo: ID3D11Device contiene los métodos gráficos a los que llama con poca frecuencia, normalmente antes de que se produzca cualquier representación, para adquirir y configurar el conjunto de recursos que necesita para empezar a dibujar píxeles. Por otro lado, ID3D11DeviceContext contiene los métodos con los que se llama a cada fotograma: para cargar en búferes, vistas y otros recursos; cambiar el estado de fusión de salida y del rasterizador; administrar los sombreadores; y dibujar los resultados del paso de esos recursos a través de los estados y sombreadores.

Una parte muy importante de este proceso es establecer el nivel de funcionalidades. El nivel de funcionalidades indica a DirectX el nivel mínimo de hardware que admite la aplicación, con D3D_FEATURE_LEVEL_9_1 como conjunto de funcionalidades más bajo y D3D_FEATURE_LEVEL_11_1 como el más alto en este momento. Debe admitir 9_1 como mínimo si quiere llegar al mayor público posible. Dedique un tiempo a leer los niveles de funcionalidades de Direct3D y a evaluar los niveles mínimo y máximo de este tipo que quiere para su juego, así como a entender las implicaciones de su elección.

Obtenga referencias (punteros) tanto al dispositivo Direct3D como al contexto del dispositivo y almacénelas como variables de nivel de clase en la instancia de DeviceResources (como punteros inteligentes ComPtr). Use estas referencias siempre que necesite acceder al contexto del dispositivo o al dispositivo Direct3D.

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

Creación de la cadena de intercambio

En este punto, tiene una ventana para dibujar y una interfaz para enviar datos y dar comandos a la GPU. Ahora, vamos a ver cómo unirlas.

En primer lugar, debe indicar a DXGI qué valores usar para las propiedades de la cadena de intercambio. Para ello, use una estructura DXGI_SWAP_CHAIN_DESC. Para las aplicaciones de escritorio, hay seis campos que son especialmente importantes:

  • Windowed: indica si la cadena de intercambio es de pantalla completa o se recorta para ajustarla a la ventana. Establézcalo en TRUE para colocar la cadena de intercambio en la ventana que ha creado anteriormente.
  • BufferUsage: establézcalo en DXGI_USAGE_RENDER_TARGET_OUTPUT. Esto indica que la cadena de intercambio será una superficie de dibujo, lo que le permite usarla como destino de representación de Direct3D.
  • SwapEffect: establézcalo en DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL.
  • Format: el formato DXGI_FORMAT_B8G8R8A8_UNORM especifica el color de 32 bits: 8 bits para cada uno de los tres canales de color RGB y 8 bits para el canal alfa.
  • BufferCount: establézcalo en 2 para obtener un comportamiento tradicional de doble búfer a fin de evitar la división. Establezca el número de búferes en 3 si el contenido gráfico tarda más de un ciclo de actualización de monitor para representar un solo fotograma (por ejemplo, a 60 Hz el umbral es superior a 16 ms).
  • SampleDesc: este campo controla el muestreo múltiple. Establezca Count en 1 y Quality en 0 para cadenas de intercambio de modelo de inversión. (Para usar el muestreo múltiple con cadenas de intercambio de modelos de inversión, dibuje un destino de representación con muestreo múltiple independiente y, a continuación, resuelva ese destino en la cadena de intercambio justo antes de su presentación. Se proporciona código de ejemplo en Muestreo múltiple en aplicaciones de la Tienda Windows.)

Después de especificar una configuración para la cadena de intercambio, debe usar el mismo generador de DXGI que creó el dispositivo Direct3D (y el contexto del dispositivo) a fin de crear dicha cadena.

Forma abreviada:

Obtenga la referencia de ID3D11Device que ha creado anteriormente. Conviértala al tipo básico IDXGIDevice3 (si no lo ha hecho aún) y, a continuación, llame a IDXGIDevice::GetAdapter para obtener el adaptador de DXGI. Obtenga el generador primario para ese adaptador mediante una llamada a IDXGIAdapter::GetParent (IDXGIAdapter hereda de IDXGIObject); ahora puede usar ese generador para crear la cadena de intercambio mediante una llamada a CreateSwapChainForHwnd, tal y como se muestra en el código de ejemplo siguiente.

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

Si es nuevo en esto, probablemente sea mejor usar la configuración que se muestra aquí. En este punto, si ya está familiarizado con las versiones anteriores de DirectX, puede que se esté preguntando: "¿Por qué no hemos creado el dispositivo y la cadena de intercambio al mismo tiempo, en lugar de pasar por todas esas clases?" La respuesta es la eficacia: las cadenas de intercambio son recursos de dispositivo Direct3D y los recursos del dispositivo están vinculados al dispositivo Direct3D determinado que los creó. Si crea un nuevo dispositivo con una nueva cadena de intercambio, tiene que volver a crear todos los recursos del dispositivo con el nuevo dispositivo Direct3D. Por lo tanto, al crear la cadena de intercambio con el mismo generador (como se muestra anteriormente), puede volver a crear la cadena de intercambio y seguir usando los recursos del dispositivo Direct3D que ya ha cargado.

Ahora tiene una ventana del sistema operativo, una manera de acceder a la GPU y a sus recursos, y una cadena de intercambio para mostrar los resultados de la representación. Lo único que queda es conectar todo entre sí.

Creación de un destino de representación para dibujar

La canalización del sombreador necesita un recurso en el que dibujar los píxeles. La forma más sencilla de crear este recurso es definir un recurso ID3D11Texture2D como búfer de reserva para que el sombreador de píxeles dibuje en él y, a continuación, leer esa textura en la cadena de intercambio.

Para ello, se crea una vista de destino de representación. En Direct3D, una vista es una forma de acceder a un recurso específico. En este caso, la vista permite que el sombreador de píxeles escriba en la textura a medida que completa sus operaciones por píxel.

Echemos un vistazo al código que se usa para ello. Al establecer DXGI_USAGE_RENDER_TARGET_OUTPUT en la cadena de intercambio, se permitió que el recurso Direct3D subyacente se usara como superficie de dibujo. Por lo tanto, para obtener la vista de destino de representación, solo tenemos que obtener el búfer de reserva de la cadena de intercambio y crear una vista de destino de representación enlazada al recurso del búfer de reserva.

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

Cree también un búfer de galería de símbolos de profundidad. Un búfer de galería de símbolos de profundidad es simplemente una forma determinada de recurso ID3D11Texture2D, que se usa normalmente para determinar qué píxeles tienen prioridad para dibujarse durante la rasterización, en función de la distancia de los objetos en la escena de la cámara. Un búfer de este tipo también se puede usar para los efectos de la galería de símbolos, donde se descartan u omiten píxeles específicos durante la rasterización. Este búfer debe tener el mismo tamaño que el destino de representación. Tenga en cuenta que no se puede leer ni representar en la textura de la galería de símbolos de profundidad del búfer de fotogramas porque la canalización del sombreador la usa de forma exclusiva antes y durante la rasterización final.

Cree también una vista para el búfer de galería de símbolos de profundidad como ID3D11DepthStencilView. La vista indica a la canalización del sombreador cómo interpretar el recurso ID3D11Texture2D subyacente; por lo tanto, si no proporciona esta vista, no se realizan pruebas de profundidad por píxel y, cuando menos, puede que los objetos de la escena parezcan estar algo descolocados.

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

El último paso es crear una ventanilla. Esto define el rectángulo visible del búfer de reserva que se muestra en la pantalla; puede cambiar la parte del búfer que se muestra en la pantalla si cambia los parámetros de la ventanilla. Este código se destina al tamaño completo de la ventana o a la resolución de pantalla, en el caso de cadenas de intercambio de pantalla completa. Por probar, cambie los valores de coordenadas proporcionados y observe los 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
    );

Así es como se pasa de no hacer nada a dibujar píxeles en una ventana. Al ponerse con ello, es buena idea familiarizarse con la forma en que DirectX (a través de DXGI) administra los recursos principales que necesita para empezar a dibujar píxeles.

A continuación, verá la estructura de la canalización de gráficos; consulte Descripción de la canalización de representación de la plantilla de aplicación de DirectX.

Siguiente

Trabajar con sombreadores y recursos de sombreador