Partager via


Utiliser des ressources d’appareil DirectX

Découvrez le rôle de Microsoft DirectX Graphic Infrastructure (DXGI) dans votre jeu DirectX sur le Windows Store. DXGI est un ensemble d’API utilisées pour configurer et gérer des ressources graphiques et des ressources d’adaptateurs graphiques de bas niveau. Sans cela, vous n’auriez aucun moyen de dessiner les graphismes de votre jeu dans une fenêtre.

Considérez DXGI de la manière suivante : pour accéder directement au GPU et gérer ses ressources, il vous fait un moyen de le décrire à votre application. L’élément d’informations le plus important dont vous avez besoin concernant le GPU est l’endroit où dessiner des pixels afin que ces derniers puissent être transmis à l’écran. En règle générale, il s’agit de la « mémoire tampon d’arrière-plan », un emplacement dans la mémoire GPU où vous pouvez dessiner les pixels, les « retourner » ou les faire « permuter », puis les envoyer à l’écran sur un signal d’actualisation. DXGI vous permet d’acquérir cet emplacement ainsi que les moyens d’utiliser cette mémoire tampon (appelée chaîne d’échange, car il s’agit d’une chaîne de mémoires tampons permutables, ce qui permet l’utilisation de plusieurs stratégies de mise en mémoire tampon).

Pour ce faire, vous devez disposer d’un accès en écriture dans la chaîne d’échange et d’une poignée dans la fenêtre qui affiche la mémoire tampon d’arrière-plan actuelle pour la chaîne d’échange. Vous devez également connecter les deux pour vous assurer que le système d’exploitation actualise la fenêtre avec le contenu de la mémoire tampon d’arrière-plan lorsque vous le demandez.

Le processus global de dessin à l’écran est le suivant :

  • Obtenez un CoreWindow pour votre application.
  • Obtenez une interface pour l’appareil et le contexte Direct3D.
  • Créez la chaîne d’échange pour afficher le rendu de votre trame dans CoreWindow.
  • Créez une cible de rendu pour dessiner et remplissez-la avec des pixels.
  • Présentez la chaîne d’échange.

Créer une fenêtre pour votre application

Tout d’abord, vous devez créer une fenêtre. Commencez par créer une classe de fenêtre en remplissant une instance de WNDCLASS, puis enregistrez-la à l’aide de RegisterClass. La classe de fenêtre contient les propriétés essentielles de la fenêtre, y compris l’icône qu’elle utilise, la fonction de traitement des messages statiques (que nous aborderons plus tard) et un nom unique pour la classe de fenêtre.

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

Ensuite, vous allez créer la fenêtre. Nous devons également fournir des informations sur la taille de la fenêtre, ainsi que le nom de la classe de fenêtre que nous venons de créer. Quand vous appelez CreateWindow, vous récupérez un pointeur opaque vers la fenêtre appelée HWND. Vous devez conserver le pointeur HWND et l’utiliser à chaque fois que vous avez besoin de faire référence à la fenêtre, y compris en cas de destruction ou de recréation, et tout particulièrement lors de la création de la chaîne d’échange DXGI dont vous vous servez pour dessiner dans la fenêtre.

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

Le modèle d’application de bureau Windows inclut un hook dans la boucle de message Windows. Vous devez baser votre boucle de programme principale hors de ce hook en écrivant une fonction « StaticWindowProc » pour traiter les événements de fenêtrage. Il doit s’agir d’une fonction statique, car Windows l’appelle en dehors du contexte de n’importe quelle instance de classe. Voici un exemple très simple de fonction de traitement des messages statiques.

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

Cet exemple simple ne vérifie que les conditions de sortie du programme : WM_CLOSE, envoyé lors d’une requête de fermeture de fenêtre et WM_DESTROY, envoyé une fois la fenêtre réellement supprimée de l’écran. Une application de production complète doit gérer d’autres événements de fenêtrage. Pour obtenir la liste complète des événements de fenêtrage, consultez Notifications de fenêtre.

La boucle de programme principale doit elle-même reconnaître les messages Windows en permettant à Windows d’exécuter la procédure de message statique. Veillez à la bonne exécution du programme en dupliquant le comportement : chaque itération doit choisir de traiter les nouveaux messages Windows s’ils sont disponibles, et si aucun message n’est présent dans la file d’attente, une nouvelle trame doit être affichée. Voici un exemple très simple :

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

Obtenir une interface pour l’appareil et le contexte Direct3D

La première étape d’utilisation de Direct3D consiste à obtenir une interface pour le matériel Direct3D (c’est-à-dire le GPU), représentée en tant qu’instances d’ID3D11Device et d’ID3D11DeviceContext. La première est une représentation virtuelle des ressources GPU. La seconde est une abstraction du pipeline et du processus de rendu, indépendante de l’appareil. Voici un moyen simple de se représenter cela : ID3D11Device contient les méthodes graphiques que vous appelez rarement, généralement avant tout rendu, pour obtenir et configurer l’ensemble de ressources dont vous avez besoin pour commencer à dessiner des pixels. Quant à ID3D11DeviceContext, il contient les méthodes que vous appelez pour chaque trame : chargement dans les tampons, les affichages et d’autres ressources, modification de l’état de la fusion de sortie et du rastériseur, gestion des nuanceurs de pixels et dessin des résultats de la transmission de ces ressources via les états et les nuanceurs de pixels.

Une partie de ce processus est essentielle : le paramétrage du niveau de fonctionnalité. Celui-ci indique à DirectX le niveau minimal du matériel pris en charge par votre application à l’aide des paramètres D3D_FEATURE_LEVEL_9_1 (ensemble des fonctionnalités minimales) et D3D_FEATURE_LEVEL_11_1 (maximum actuel). Pour atteindre le public le plus large possible, vous devez au moins prendre en charge 9_1. Prenez un moment pour examiner les niveaux de fonctionnalités Direct3D, évaluer vous-même les niveaux de fonctionnalités minimal et maximal que votre jeu doit prendre en charge, et comprendre ce qu’impliquent vos choix.

Obtenez des références (pointeurs) vers l’appareil Direct3D ainsi que vers le contexte de l’appareil, puis stockez-les en tant que variables de niveau classe sur l’instance DeviceResources (en tant que pointeurs intelligents ComPtr). Utilisez ces références chaque fois que vous devez accéder à l’appareil Direct3D ou au contexte de l’appareil.

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

Créer la chaîne d’échange

À ce stade, vous disposez d’une fenêtre dans laquelle dessiner, ainsi que d’une interface pour envoyer des données et fournir des commandes au GPU. Voyons maintenant comment les rassembler.

Tout d’abord, vous devez indiquer à DXGI quelles valeurs utiliser pour les propriétés de la chaîne d’échange. Pour ce faire, utilisez une structure DXGI_SWAP_CHAIN_DESC. Six champs sont particulièrement importants pour les applications de bureau :

  • Windowed : indique si la chaîne d’échange est en plein écran ou attachée à la fenêtre. Définissez cette valeur sur TRUE pour placer la chaîne d’échange dans la fenêtre que vous avez créée précédemment.
  • BufferUsage : définissez cette valeur sur DXGI_USAGE_RENDER_TARGET_OUTPUT. Cela indique que la chaîne d’échange est une surface de dessin, ce qui vous permet de l’utiliser comme cible de rendu Direct3D.
  • SwapEffect : définissez cette valeur sur DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL.
  • Format : le format DXGI_FORMAT_B8G8R8A8_UNORM spécifie la couleur 32 bits (8 bits pour chacun des trois canaux de couleur RVB et 8 bits pour le canal alpha).
  • BufferCount : définissez cette valeur sur 2 pour un comportement à double mémoire tampon classique afin d’éviter les problèmes de déchirure. Définissez le nombre de tampons sur 3 si votre contenu graphique prend plusieurs cycles d’actualisation du moniteur pour afficher une trame unique (par exemple, à 60 Hz, le seuil est supérieur à 16 ms).
  • SampleDesc : ce champ contrôle l’échantillonnage multiple. Définissez Count sur 1 et Quality sur 0 pour les chaînes d’échange de modèle inversé. (Pour utiliser l’échantillonnage multiple avec des chaînes d’échange de modèle inversé, dessinez sur une cible de rendu distincte avec plusieurs échantillons, puis résolvez cette cible vers la chaîne d’échange juste avant de la présenter. Un exemple de code est disponible dans Échantillonnage multiple dans les applications du Windows Store.)

Une fois que vous avez spécifié une configuration pour la chaîne d’échange, vous devez utiliser la même fabrique DXGI qui a permis la création de l’appareil Direct3D (ainsi que le contexte de l’appareil) pour créer la chaîne d’échange.

Forme abrégée :

Obtenez la référence ID3D11Device que vous avez créée précédemment. Si vous ne l’avez pas déjà fait, convertissez-la en IDXGIDevice3, puis appelez IDXGIDevice::GetAdapter pour obtenir l’adaptateur DXGI. Obtenez la fabrique parente de cet adaptateur en appelant IDXGIAdapter::GetParent (IDXGIAdapter hérite d’IDXGIObject). Vous pouvez maintenant utiliser cette fabrique pour créer la chaîne d’échange en appelant CreateSwapChainForHwnd, comme indiqué dans l’exemple de code suivant.

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 vous débutez, il est probablement préférable d’utiliser la configuration indiquée ici. À ce stade, si vous êtes déjà familiarisé avec les versions précédentes de DirectX, vous vous demandez peut-être pourquoi ne pas avoir créé l’appareil et la chaîne d’échange en même temps, au lieu de revenir sur toutes ces classes. La réponse tient en un mot : l’efficacité. En effet, les chaînes d’échange sont des ressources d’appareil Direct3D et les ressources d’appareil sont liées à l’appareil Direct3D particulier qui les a créés. Si vous créez un appareil avec une nouvelle chaîne d’échange, vous devez recréer toutes vos ressources d’appareil à l’aide du nouvel appareil Direct3D. Ainsi, en créant la chaîne d’échange avec la même fabrique (comme indiqué ci-dessus), vous pouvez recréer la chaîne d’échange et continuer à utiliser les ressources d’appareil Direct3D que vous avez déjà chargées.

Vous disposez désormais d’une fenêtre accessible à partir du système d’exploitation, d’un moyen d’accéder au GPU et à ses ressources et d’une chaîne d’échange pour afficher les résultats de rendu. Il ne vous reste plus qu’à relier tous les éléments.

Créer une cible de rendu pour le dessin

Le pipeline du nuanceur de pixels a besoin d’une ressource pour pouvoir dessiner des pixels. La façon la plus simple de créer cette ressource consiste à définir une ressource ID3D11Texture2D en tant que mémoire tampon d’arrière-plan pour que le nuanceur de pixels puisse y dessiner, puis de lire cette texture dans la chaîne d’échange.

Pour ce faire, créez une vue de cible de rendu. Dans Direct3D, une vue est un moyen d’accéder à une ressource spécifique. Dans ce cas, la vue permet au nuanceur de pixels d’écrire dans la texture à mesure qu’il termine ses opérations par pixel.

Examinons le code correspondant. Quand vous paramétrez DXGI_USAGE_RENDER_TARGET_OUTPUT sur la chaîne d’échange, vous avez activé la ressource Direct3D sous-jacente à utiliser comme surface de dessin. Par conséquent, pour obtenir notre vue de cible de rendu, il suffit de récupérer la mémoire tampon d’arrière-plan à partir de la chaîne d’échange et de créer une vue de cible de rendu liée à la ressource de mémoire tampon d’arrière-plan.

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

Créez également une mémoire tampon de profondeur et de gabarit. Une mémoire tampon de profondeur et de gabarit n’est qu’une forme particulière de ressource ID3D11Texture2D, qui est généralement utilisée pour déterminer quels pixels sont prioritaires lors de la rastérisation en fonction de la distance entre les objets de la scène et la caméra. Une mémoire tampon de profondeur et de gabarit peut également être utilisé pour gérer les effets de gabarit, où des pixels spécifiques sont abandonnés ou ignorés pendant la rastérisation. Cette mémoire tampon doit être de la même taille que la cible de rendu. Notez que vous ne pouvez ni lire ni afficher la texture de profondeur et de gabarit de la mémoire tampon de trame, car celle-ci est utilisée exclusivement par le pipeline du nuanceur de pixels avant et pendant la rastérisation finale.

Créez également une vue en tant que ID3D11DepthStencilView pour la mémoire tampon de profondeur et de gabarit. La vue indique au pipeline du nuanceur de pixels comment interpréter la ressource ID3D11Texture2D sous-jacente. Par conséquent, si vous ne fournissez pas ce mode, aucun test de profondeur par pixel n’est effectué, et les objets de votre scène peuvent sembler au mieux légèrement à l’envers.

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

La dernière étape consiste à créer une fenêtre d’affichage. Cela permet de définir le rectangle visible de la mémoire tampon d’arrière-plan affichée à l’écran. Vous pouvez modifier la partie de la mémoire tampon affichée à l’écran en modifiant les paramètres de la fenêtre d’affichage. Ce code cible la taille entière de la fenêtre, ou la résolution d’écran dans le cas de chaînes d’échange en mode plein écran. Amusez-vous à modifier les valeurs des coordonnées fournies et observez les résultats.

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

Voilà, c’est ainsi qu’en partant de zéro, vous allez commencer à dessiner des pixels dans une fenêtre ! À mesure que vous progressez, nous vous recommandons de vous familiariser avec la façon dont DirectX, via DXGI, gère les ressources principales dont vous avez besoin pour commencer à dessiner des pixels.

Vous allez ensuite examiner la structure du pipeline graphique. Consultez Comprendre le pipeline de rendu du modèle d’application DirectX.

Ressource suivante

Utilisation des nuanceurs et des ressources de nuanceur