Usare le risorse del dispositivo DirectX

Comprendere il ruolo di Microsoft DirectX Graphics Infrastructure (DXGI) Windows nel gioco DirectX Store DirectX. DXGI è un set di API usate per configurare e gestire le risorse della scheda grafica e della scheda grafica di basso livello. Senza di esso, non avresti modo di disegnare la grafica del tuo gioco in una finestra.

Si pensi a DXGI in questo modo: per accedere direttamente alla GPU e gestire le relative risorse, è necessario avere un modo per descriverlo all'app. La parte più importante di informazioni necessarie sulla GPU è la posizione per disegnare pixel in modo che possa inviare tali pixel allo schermo. In genere viene chiamato il "buffer indietro", una posizione nella memoria GPU in cui è possibile disegnare i pixel e quindi "capovolgere" o "scambiata" e inviare allo schermo un segnale di aggiornamento. DXGI consente di acquisire tale posizione e il mezzo per usare tale buffer (denominato catena di scambio perché è una catena di buffer scambiabili, consentendo più strategie di buffering).

A tale scopo, è necessario accedere alla catena di scambio e un handle alla finestra che visualizzerà il buffer back corrente per la catena di scambio. È anche necessario connettere i due per assicurarsi che il sistema operativo aggiornerà la finestra con il contenuto del buffer indietro quando lo si richiede.

Il processo complessivo per il disegno sullo schermo è il seguente:

  • Ottenere un coreWindow per l'app.
  • Ottenere un'interfaccia per il dispositivo Direct3D e il contesto.
  • Creare la catena di scambio per visualizzare l'immagine di cui è stato eseguito il rendering in CoreWindow.
  • Creare una destinazione di rendering per il disegno e popolarla con pixel.
  • Presentare la catena di scambio!

Creare una finestra per l'app

La prima cosa che dobbiamo fare è creare una finestra. Prima di tutto, creare una classe di finestra popolando un'istanza di WNDCLASS, quindi registrarla usando RegisterClass. La classe window contiene proprietà essenziali della finestra, inclusa l'icona usata, la funzione di elaborazione dei messaggi statici (più avanti) e un nome univoco per la classe finestra.

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

Successivamente, si crea la finestra. È anche necessario fornire informazioni sulle dimensioni per la finestra e il nome della classe di finestra appena creata. Quando si chiama CreateWindow, viene restituito un puntatore opaco alla finestra denominata HWND; È necessario mantenere il puntatore HWND e usarlo qualsiasi volta che è necessario fare riferimento alla finestra, inclusa l'eliminazione o la ricreazione, e (soprattutto importante) quando si crea la catena di scambio DXGI usata per disegnare nella finestra.

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

Il modello di app desktop Windows include un hook nel ciclo di messaggi Windows. È necessario basare il ciclo principale del programma fuori da questo hook scrivendo una funzione "StaticWindowProc" per elaborare gli eventi di finestra. Questa deve essere una funzione statica perché Windows lo chiamerà al di fuori del contesto di qualsiasi istanza di classe. Ecco un esempio molto semplice di una funzione di elaborazione dei messaggi statici.

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

Questo semplice esempio verifica solo le condizioni di uscita dal programma: WM_CLOSE, inviato quando la finestra viene richiesta per essere chiusa e WM_DESTROY, che viene inviata dopo che la finestra viene effettivamente rimossa dalla schermata. Un'app di produzione completa deve gestire anche altri eventi di finestra, per l'elenco completo degli eventi di finestra, vedere Notifiche finestra.

Il ciclo principale del programma deve riconoscere Windows messaggi consentendo Windows l'opportunità di eseguire il processo di messaggio statico. Aiutare il programma a eseguire in modo efficiente eseguendo il fork del comportamento: ogni iterazione deve scegliere di elaborare nuovi messaggi Windows se sono disponibili e se non sono presenti messaggi nella coda, dovrebbe eseguire il rendering di un nuovo frame. Ecco un esempio molto semplice:

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

Ottenere un'interfaccia per il dispositivo Direct3D e il contesto

Il primo passaggio da usare Direct3D consiste nell'acquisire un'interfaccia per l'hardware Direct3D (GPU), rappresentata come istanze di ID3D11Device e ID3D11DeviceContext. Il primo è una rappresentazione virtuale delle risorse gpu e quest'ultima è un'astrazione agnostica del dispositivo della pipeline di rendering e del processo. Ecco un modo semplice per pensarlo: ID3D11Device contiene i metodi grafici che si chiamano raramente, in genere prima che si verifichi un rendering, per acquisire e configurare il set di risorse che è necessario avviare il disegno pixel. ID3D11DeviceContext, invece, contiene i metodi che si chiamano ogni frame: caricamento in buffer e visualizzazioni e altre risorse, modifica dello stato di fusione di output e rasterizzatore, gestione degli shader e disegno dei risultati del passaggio di tali risorse attraverso gli stati e gli shader.

C'è una parte molto importante di questo processo: impostando il livello di funzionalità. Il livello di funzionalità indica a DirectX il livello minimo di hardware supportato dall'app, con D3D_FEATURE_LEVEL_9_1 come set di funzionalità più basso e D3D_FEATURE_LEVEL_11_1 come il livello corrente più alto. È consigliabile supportare 9_1 come minimo se si vuole raggiungere il pubblico più ampio possibile. È necessario leggere i livelli di funzionalità Direct3D e valutare autonomamente i livelli minimi e massimi di funzionalità che si desidera supportare e comprendere le implicazioni della propria scelta.

Ottenere riferimenti (puntatori) sia al contesto del dispositivo Direct3D che al contesto del dispositivo e archiviarli come variabili a livello di classe nell'istanza di DeviceResources (come puntatori intelligenti ComPtr ). Usare questi riferimenti ogni volta che è necessario accedere al contesto del dispositivo o del dispositivo Direct3D.

D3D_FEATURE_LEVEL levels[] = {
    D3D_FEATURE_LEVEL_9_1,
    D3D_FEATURE_LEVEL_9_2,
    D3D_FEATURE_LEVEL_9_3,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_11_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);

Creare la catena di scambio

Ok: è disponibile una finestra in cui disegnare e si dispone di un'interfaccia per inviare dati e assegnare comandi alla GPU. Ora vediamo come raggrupparli.

In primo luogo, si indica a DXGI quali valori usare per le proprietà della catena di scambio. Eseguire questa operazione usando una struttura DXGI_SWAP_CHAIN_DESC . Sei campi sono particolarmente importanti per le app desktop:

  • Finestrata: indica se la catena di scambio è a schermo intero o ritagliata nella finestra. Impostare questa opzione su TRUE per inserire la catena di scambio nella finestra creata in precedenza.
  • BufferUsage: impostarlo su DXGI_USAGE_RENDER_TARGET_OUTPUT. Ciò indica che la catena di scambio sarà una superficie di disegno, consentendo di usarla come destinazione di rendering Direct3D.
  • SwapEffect: impostarlo su DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL.
  • Formato: il formato DXGI_FORMAT_B8G8R8A8_UNORM specifica il colore a 32 bit: 8 bit per ognuno dei tre canali di colore RGB e 8 bit per il canale alfa.
  • BufferCount: impostare questa opzione su 2 per un comportamento tradizionale con buffer doppio per evitare la rottura. Impostare il conteggio del buffer su 3 se il contenuto grafico richiede più di un ciclo di aggiornamento del monitoraggio per eseguire il rendering di un singolo frame (ad esempio a 60 Hz, la soglia è superiore a 16 ms).
  • SampleDesc: questo campo controlla il multicampionamento. Impostare Count su 1 e Quality su 0 per le catene di scambio a capovolgimento. Per usare multicampionamento con catene di scambio di modelli a capovolgimento, disegnare su una destinazione di rendering multicampionata separata e quindi risolvere tale destinazione alla catena di scambio appena prima di presentarla. Il codice di esempio viene fornito in Multisampling nelle app Windows Store.

Dopo aver specificato una configurazione per la catena di scambio, è necessario usare la stessa factory DXGI che ha creato il dispositivo Direct3D (e il contesto del dispositivo) per creare la catena di scambio.

**Modulo breve: **

Ottenere il riferimento ID3D11Device creato in precedenza. Esegui l'upcast in IDXGIDevice3 (se non è già stato) e quindi chiama IDXGIDevice::GetAdapter per acquisire l'adapter DXGI. Ottenere la factory padre per tale adattatore chiamando IDXGIFactory2::GetParent (IDXGIFactory2 eredita da IDXGIObject)— ora è possibile usare tale factory per creare la catena di scambio chiamando CreateSwapChainForHwnd, come illustrato nell'esempio di codice seguente.

DXGI_SWAP_CHAIN_DESC desc;
ZeroMemory(&desc, sizeof(DXGI_SWAP_CHAIN_DESC));
desc.Windowed = TRUE; // Sets the initial state of full-screen mode.
desc.BufferCount = 2;
desc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.SampleDesc.Count = 1;      //multisampling setting
desc.SampleDesc.Quality = 0;    //vendor-specific flag
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
desc.OutputWindow = hWnd;

// Create the DXGI device object to use in other factories, such as Direct2D.
Microsoft::WRL::ComPtr<IDXGIDevice3> dxgiDevice;
m_pd3dDevice.As(&dxgiDevice);

// Create swap chain.
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
Microsoft::WRL::ComPtr<IDXGIFactory> factory;

hr = dxgiDevice->GetAdapter(&adapter);

if (SUCCEEDED(hr))
{
    adapter->GetParent(IID_PPV_ARGS(&factory));

    hr = factory->CreateSwapChain(
        m_pd3dDevice.Get(),
        &desc,
        &m_pDXGISwapChain
        );
}

Se si sta appena iniziando, è probabilmente preferibile usare la configurazione illustrata qui. A questo punto, se si ha già familiarità con le versioni precedenti di DirectX si potrebbe chiedere: "Perché non è stato creato il dispositivo e lo scambio contemporaneamente, invece di tornare indietro attraverso tutte queste classi?" La risposta è efficienza: le catene di scambio sono risorse del dispositivo Direct3D e le risorse del dispositivo sono associate al particolare dispositivo Direct3D che li ha creati. Se si crea un nuovo dispositivo con una nuova catena di scambio, è necessario ricreare tutte le risorse del dispositivo usando il nuovo dispositivo Direct3D. Quindi creando la catena di scambio con la stessa factory (come illustrato sopra), è possibile ricreare la catena di scambio e continuare a usare le risorse del dispositivo Direct3D già caricate!

A questo punto è disponibile una finestra dal sistema operativo, un modo per accedere alla GPU e alle relative risorse e una catena di scambio per visualizzare i risultati del rendering. Tutto ciò che è lasciato è quello di collegare l'intera cosa insieme!

Creare una destinazione di rendering per il disegno

La pipeline shader necessita di una risorsa in cui disegnare pixel. Il modo più semplice per creare questa risorsa consiste nel definire una risorsa ID3D11Texture2D come buffer back per il pixel shader da disegnare e quindi leggere tale trama nella catena di scambio.

A tale scopo, si crea una visualizzazione di destinazione di rendering. In Direct3D una visualizzazione è un modo per accedere a una risorsa specifica. In questo caso, la visualizzazione consente al pixel shader di scrivere nella trama man mano che completa le operazioni per pixel.

Esaminiamo il codice per questo. Quando si imposta DXGI_USAGE_RENDER_TARGET_OUTPUT nella catena di scambio, che ha abilitato la risorsa Direct3D sottostante da usare come superficie di disegno. Per ottenere la visualizzazione di destinazione del rendering, è sufficiente recuperare il buffer indietro dalla catena di scambio e creare una visualizzazione di destinazione di rendering associata alla risorsa buffer back.

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

Creare anche un buffer di profondità-stencil. Un buffer di profondità-stencil è solo una forma particolare di risorsa ID3D11Texture2D , che viene in genere usata per determinare quali pixel hanno priorità di disegno durante la rasterizzazione in base alla distanza degli oggetti nella scena dalla fotocamera. Un buffer stencil di profondità può essere usato anche per gli effetti stencil, in cui i pixel specifici vengono ignorati o ignorati durante la rasterizzazione. Questo buffer deve essere la stessa dimensione della destinazione di rendering. Si noti che non è possibile leggere o eseguire il rendering nella trama della profondità dello stencil del buffer frame perché viene usato esclusivamente dalla pipeline shader prima e durante la rasterizzazione finale.

Creare anche una vista per il buffer depth-stencil come ID3D11DepthStencilView. La visualizzazione indica alla pipeline shader come interpretare la risorsa ID3D11Texture2D sottostante, quindi se non si fornisce questa visualizzazione non viene eseguito alcun test di profondità per pixel e gli oggetti nella scena potrebbero sembrare un po' all'interno almeno!

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

L'ultimo passaggio consiste nel creare un viewport. Questo definisce il rettangolo visibile del buffer indietro visualizzato sullo schermo; è possibile modificare la parte del buffer visualizzato sullo schermo modificando i parametri del viewport. Questo codice è destinato all'intera dimensione della finestra o alla risoluzione dello schermo, nel caso di catene di scambio a schermo intero. Per divertimento, modificare i valori di coordinate forniti e osservare i risultati.

ZeroMemory(&m_viewport, sizeof(D3D11_VIEWPORT));
m_viewport.Height = (float) m_bbDesc.Height;
m_viewport.Width = (float) m_bbDesc.Width;
m_viewport.MinDepth = 0;
m_viewport.MaxDepth = 1;

m_pd3dDeviceContext->RSSetViewports(
    1,
    &m_viewport
    );

E questo è come si passa da niente a disegnare pixel in una finestra! Come si inizia, è consigliabile acquisire familiarità con il modo in cui DirectX, tramite DXGI, gestisce le risorse principali necessarie per avviare il disegno di pixel.

Successivamente si esaminerà la struttura della pipeline grafica; vedere Informazioni sulla pipeline di rendering del modello di app DirectX.

Successivo

Usare le risorse shader e shader