Usare le risorse dei dispositivi DirectX
Comprendere il ruolo di Microsoft DirectX Graphics Infrastructure (DXGI) nel gioco DirectX di Windows Store. DXGI è un set di API usate per configurare e gestire risorse di schede grafiche e schede grafiche 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 in cui disegnare pixel in modo che possa inviare tali pixel allo schermo. In genere si tratta del "buffer nascosto", ovvero una posizione nella memoria GPU in cui è possibile disegnare i pixel e quindi "capovolgere" o "scambiata" e inviarla allo schermo su un segnale di aggiornamento. DXGI consente di acquisire tale posizione e i mezzi per usare tale buffer (denominata catena di scambio perché si tratta di 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 nascosto corrente per la catena di scambio. È anche necessario connettere i due per assicurarsi che il sistema operativo aggiorni la finestra con il contenuto del buffer nascosto quando lo si richiede a tale scopo.
Il processo complessivo per il disegno sullo schermo è il seguente:
- Ottenere un CoreWindow per l'app.
- Ottenere un'interfaccia per il dispositivo e il contesto Direct3D.
- Creare la catena di scambio per visualizzare l'immagine sottoposta a rendering in CoreWindow.
- Creare una destinazione di rendering per il disegno e popolarla con pixel.
- Presenta la catena di scambio!
Creare una finestra per l'app
La prima cosa da fare è creare una finestra. Prima di tutto, creare una classe finestra popolando un'istanza di WNDCLASS, quindi registrarla usando RegisterClass. La classe window contiene proprietà essenziali della finestra, tra cui l'icona usata, la funzione di elaborazione dei messaggi statici (più avanti) e un nome univoco per la classe 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);
}
Successivamente, si crea la finestra. È anche necessario fornire informazioni sulle dimensioni per la finestra e il nome della classe finestra appena creata. Quando chiami CreateWindow, torna alla finestra un puntatore opaco denominato HWND. Dovrai mantenere il puntatore HWND e usarlo ogni volta che devi fare riferimento alla finestra, inclusa l'eliminazione o la ricreazione, e (soprattutto importante) quando crei la catena di scambio DXGI che usi 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 di Windows include un hook nel ciclo dei messaggi di Windows. Dovrai basare il ciclo principale di questo hook scrivendo una funzione "StaticWindowProc" per elaborare gli eventi di windowing. Deve trattarsi di una funzione statica perché Windows lo chiamerà all'esterno 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 controlla solo le condizioni di uscita del programma: WM_CLOedizione Standard, inviato quando la finestra viene richiesta di essere chiusa e WM_DESTROY, che viene inviato dopo che la finestra viene effettivamente rimossa dallo schermo. Un'app di produzione completa deve gestire anche altri eventi di windowing. Per l'elenco completo degli eventi di windowing, vedi Notifiche finestra.
Il ciclo principale del programma stesso deve riconoscere i messaggi di Windows consentendo a Windows di eseguire la procedura statica del messaggio. Aiuta il programma a essere eseguito in modo efficiente tramite la copia tramite fork del comportamento: ogni iterazione deve scegliere di elaborare nuovi messaggi di Windows, se 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 e il contesto Direct3D
Il primo passaggio per usare Direct3D consiste nell'acquisire un'interfaccia per l'hardware Direct3D (GPU), rappresentato come istanze di ID3D11Device e ID3D11DeviceContext. Il primo è una rappresentazione virtuale delle risorse GPU e quest'ultimo è un'astrazione indipendente dal dispositivo della pipeline di rendering e del processo. Ecco un modo semplice per considerarlo: ID3D11Device contiene i metodi grafici che chiami raramente, in genere prima che si verifichi un rendering, per acquisire e configurare il set di risorse necessarie per iniziare a disegnare pixel. ID3D11DeviceContext, d'altra parte, contiene i metodi che chiami ogni fotogramma: caricamento in buffer e visualizzazioni e altre risorse, modifica dello stato di unione e rasterizzatore di output, gestione degli shader e disegno dei risultati del passaggio di tali risorse tramite gli stati e gli shader.
C'è una parte molto importante di questo processo: l'impostazione del 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 massimo corrente. È consigliabile supportare 9_1 come minimo se si vuole raggiungere il pubblico più ampio possibile. Dedicare del tempo per leggere i livelli di funzionalità direct3D e valutare per se stessi i livelli di funzionalità minimi e massimi che vuoi che il tuo gioco supporti e comprendere le implicazioni della tua scelta.
Ottenere riferimenti (puntatori) sia al dispositivo Direct3D che al contesto di dispositivo e archiviarli come variabili a livello di classe nell'istanza deviceResources (come puntatori intelligenti ComPtr ). Usare questi riferimenti ogni volta che è necessario accedere al dispositivo Direct3D o al contesto di dispositivo.
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);
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. Vediamo ora come riunirli.
In primo luogo, devi indicare a DXGI quali valori usare per le proprietà della catena di scambio. Eseguire questa operazione usando una struttura di DXGI_SWAP_CHAIN_DESC . Sei campi sono particolarmente importanti per le app desktop:
- Finestra: indica se la catena di scambio è a schermo intero o ritagliata nella finestra. Impostare su TRUE per inserire la catena di scambio nella finestra creata in precedenza.
- BufferUsage: impostare questa opzione 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: impostare questa opzione su DXGI_SWAP_EFFECT_FLIP_edizione Standard QUENTIAL.
- 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 su 2 per un comportamento tradizionale con doppio buffer per evitare l'eliminazione. Impostare il numero di buffer su 3 se il contenuto grafico richiede più di un ciclo di aggiornamento del monitor per eseguire il rendering di un singolo fotogramma (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 di modelli capovolti. Per usare il multicampionamento con catene di scambio di modelli capovolti, disegnare su una destinazione di rendering multicampionata separata e quindi risolvere tale destinazione alla catena di scambio subito prima di presentarla. Il codice di esempio viene fornito in Multicampionamento nelle app di 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 di dispositivo) per creare la catena di scambio.
Forma breve:
Ottenere il riferimento ID3D11Device creato in precedenza. Eseguirne l'upcast in IDXGIDevice3 (se non è già stato fatto) e quindi chiamare IDXGIDevice::GetAdapter per acquisire l'adattatore DXGI. Ottenere la factory padre per tale adattatore chiamando IDXGIAdapter::GetParent (IDXGIAdapter 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 iniziando, è probabilmente preferibile usare la configurazione illustrata qui. A questo punto, se hai già familiarità con le versioni precedenti di DirectX potresti chiedere: "Perché non abbiamo creato il dispositivo e scambiamo la catena contemporaneamente, invece di tornare indietro in tutte queste classi?" La risposta è l'efficienza: le catene di scambio sono risorse del dispositivo Direct3D e le risorse del dispositivo sono collegate al particolare dispositivo Direct3D che li ha creati. Se crei un nuovo dispositivo con una nuova catena di scambio, devi ricreare tutte le risorse del dispositivo usando il nuovo dispositivo Direct3D. Creando quindi la catena di scambio con la stessa factory (come illustrato in precedenza), è 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 di rendering. Tutto quello che rimane è quello di collegare tutta la cosa insieme!
Creare una destinazione di rendering per il disegno
La pipeline dello shader richiede una risorsa in cui disegnare pixel. Il modo più semplice per creare questa risorsa consiste nel definire una risorsa ID3D11Texture2D come buffer nascosto in cui disegnare il pixel shader e quindi leggere tale trama nella catena di scambio.
A tale scopo, creare 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 mentre completa le operazioni per pixel.
Di seguito viene esaminato il codice. Quando si imposta DXGI_USAGE_RENDER_TARGET_OUTPUT sulla catena di scambio, che ha abilitato l'uso della risorsa Direct3D sottostante come superficie di disegno. Per ottenere la visualizzazione di destinazione di rendering, è sufficiente ottenere il buffer nascosto dalla catena di scambio e creare una visualizzazione di destinazione di rendering associata alla risorsa buffer nascosto.
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 depth-stencil. Un buffer depth-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. È anche possibile usare un buffer depth-stencil per gli effetti degli stencil, in cui i pixel specifici vengono ignorati o ignorati durante la rasterizzazione. Questo buffer deve avere le stesse dimensioni della destinazione di rendering. Si noti che non è possibile leggere o eseguire il rendering nella trama depth-stencil del buffer dei frame perché viene usato esclusivamente dalla pipeline dello shader prima e durante la rasterizzazione finale.
Creare anche una visualizzazione per il buffer depth-stencil come ID3D11DepthStencilView. La visualizzazione indica alla pipeline dello 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 nascosto visualizzato sullo schermo; è possibile modificare la parte del buffer visualizzata sullo schermo modificando i parametri del riquadro di visualizzazione. Questo codice ha come destinazione l'intera dimensione della finestra, o la risoluzione dello schermo, in caso di catene di scambio a schermo intero. Per divertimento, modificare i valori delle coordinate fornite 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
);
Ed è così che vai da niente a disegnare pixel in una finestra! All'inizio, è consigliabile acquisire familiarità con il modo in cui DirectX, tramite DXGI, gestisce le risorse principali necessarie per iniziare a disegnare pixel.
Successivamente si esaminerà la struttura della pipeline grafica; vedere Informazioni sulla pipeline di rendering del modello di app DirectX.
Argomenti correlati