Bekerja dengan sumber daya perangkat DirectX

Pahami peran Microsoft DirectX Graphics Infrastructure (DXGI) di game DirectX Windows Store Anda. DXGI adalah sekumpulan API yang digunakan untuk mengonfigurasi dan mengelola sumber daya adaptor grafis dan grafis tingkat rendah. Tanpa itu, Anda tidak akan memiliki cara untuk menggambar grafis game Anda ke jendela.

Pikirkan DXGI dengan cara ini: untuk langsung mengakses GPU dan mengelola sumber dayanya, Anda harus memiliki cara untuk menggambarkannya ke aplikasi Anda. Bagian terpenting dari info yang Anda butuhkan tentang GPU adalah tempat untuk menggambar piksel sehingga dapat mengirim piksel tersebut ke layar. Biasanya ini disebut "buffer belakang"—lokasi dalam memori GPU tempat Anda dapat menggambar piksel dan kemudian membuatnya "dibalik" atau "ditukar" dan dikirim ke layar pada sinyal refresh. DXGI memungkinkan Anda memperoleh lokasi tersebut dan sarana untuk menggunakan buffer tersebut (disebut rantai pertukaran karena merupakan rantai buffer yang dapat ditukar, memungkinkan beberapa strategi buffering).

Untuk melakukan ini, Anda memerlukan akses untuk menulis ke rantai pertukaran, dan handel ke jendela yang akan menampilkan buffer belakang saat ini untuk rantai pertukaran. Anda juga perlu menyambungkan keduanya untuk memastikan bahwa sistem operasi akan me-refresh jendela dengan konten buffer belakang ketika Anda memintanya untuk melakukannya.

Proses keseluruhan untuk menggambar ke layar adalah sebagai berikut:

  • Dapatkan CoreWindow untuk aplikasi Anda.
  • Dapatkan antarmuka untuk perangkat dan konteks Direct3D.
  • Buat rantai pertukaran untuk menampilkan gambar yang Anda render di CoreWindow.
  • Buat target render untuk menggambar dan mengisinya dengan piksel.
  • Sajikan rantai pertukaran!

Membuat jendela untuk aplikasi Anda

Hal pertama yang perlu kita lakukan adalah membuat jendela. Pertama, buat kelas jendela dengan mengisi instans WNDCLASS, lalu daftarkan menggunakan RegisterClass. Kelas jendela berisi properti penting dari jendela, termasuk ikon yang digunakannya, fungsi pemrosesan pesan statis (lebih lanjut tentang ini nanti), dan nama unik untuk kelas jendela.

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

Selanjutnya, Anda membuat jendela. Kita juga perlu memberikan informasi ukuran untuk jendela dan nama kelas jendela yang baru saja kita buat. Ketika Anda memanggil CreateWindow, Anda mendapatkan kembali pointer buram ke jendela yang disebut HWND; Anda harus menyimpan pointer HWND dan menggunakannya kapan saja Anda perlu mereferensikan jendela, termasuk menghancurkan atau membuatnya kembali, dan (terutama penting) saat membuat rantai pertukaran DXGI yang Anda gunakan untuk menggambar di jendela.

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

Model aplikasi desktop Windows menyertakan hook ke dalam perulangan pesan Windows. Anda harus mendasarkan loop program utama Anda dari kait ini dengan menulis fungsi "StaticWindowProc" untuk memproses peristiwa windowing. Ini harus menjadi fungsi statis karena Windows akan memanggilnya di luar konteks instans kelas apa pun. Berikut adalah contoh yang sangat sederhana dari fungsi pemrosesan pesan statis.

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

Contoh sederhana ini hanya memeriksa kondisi keluar program: WM_CLOSE, dikirim ketika jendela diminta untuk ditutup, dan WM_DESTROY, yang dikirim setelah jendela benar-benar dihapus dari layar. Aplikasi produksi lengkap juga perlu menangani peristiwa windowing lainnya—untuk daftar lengkap peristiwa windowing, lihat Pemberitahuan Jendela.

Perulangan program utama itu sendiri perlu mengakui pesan Windows dengan memungkinkan Windows kesempatan untuk menjalankan proc pesan statis. Bantu program berjalan secara efisien dengan forking perilaku: setiap iterasi harus memilih untuk memproses pesan Windows baru jika tersedia, dan jika tidak ada pesan dalam antrean, itu harus merender bingkai baru. Berikut adalah contoh yang sangat sederhana:

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

Mendapatkan antarmuka untuk perangkat dan konteks Direct3D

Langkah pertama untuk menggunakan Direct3D adalah memperoleh antarmuka untuk perangkat keras Direct3D (GPU), yang diwakili sebagai instans ID3D11Device dan ID3D11DeviceContext. Yang pertama adalah representasi virtual dari sumber daya GPU, dan yang terakhir adalah abstraksi agnostik perangkat dari alur dan proses penyajian. Berikut adalah cara mudah untuk memikirkannya: ID3D11Device berisi metode grafis yang jarang Anda panggil, biasanya sebelum penyajian terjadi, untuk memperoleh dan mengonfigurasi serangkaian sumber daya yang Anda butuhkan untuk mulai menggambar piksel. ID3D11DeviceContext, di sisi lain, berisi metode yang Anda panggil setiap bingkai: memuat buffer dan tampilan dan sumber daya lainnya, mengubah status output-merger dan rasterizer, mengelola shader, dan menggambar hasil melewati sumber daya tersebut melalui status dan shader.

Ada satu bagian yang sangat penting dari proses ini: mengatur tingkat fitur. Tingkat fitur memberi tahu DirectX tingkat minimum perangkat keras yang didukung aplikasi Anda, dengan D3D_FEATURE_LEVEL_9_1 sebagai set fitur terendah dan D3D_FEATURE_LEVEL_11_1 sebagai yang tertinggi saat ini. Anda harus mendukung 9_1 sebagai minimum jika Anda ingin menjangkau audiens seluas mungkin. Luangkan waktu untuk membaca tingkat fitur Direct3D dan menilai sendiri tingkat fitur minimum dan maksimum yang ingin Anda dukung dan untuk memahami implikasi pilihan Anda.

Dapatkan referensi (pointer) ke konteks perangkat dan perangkat Direct3D dan simpan sebagai variabel tingkat kelas pada instans DeviceResources (sebagai pointer pintar ComPtr ). Gunakan referensi ini setiap kali Anda perlu mengakses perangkat Direct3D atau konteks perangkat.

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

Membuat rantai pertukaran

Oke: Anda memiliki jendela untuk menggambar, dan Anda memiliki antarmuka untuk mengirim data dan memberikan perintah ke GPU. Sekarang mari kita lihat cara menyatukannya.

Pertama, Anda memberi tahu DXGI nilai apa yang akan digunakan untuk properti rantai pertukaran. Lakukan ini menggunakan struktur DXGI_SWAP_CHAIN_DESC. Enam bidang sangat penting untuk aplikasi desktop:

  • Berjendela: Menunjukkan apakah rantai pertukaran adalah layar penuh atau diklip ke jendela. Atur ini ke TRUE untuk menempatkan rantai pertukaran di jendela yang Anda buat sebelumnya.
  • BufferUsage: Atur ini ke DXGI_USAGE_RENDER_TARGET_OUTPUT. Ini menunjukkan bahwa rantai pertukaran akan menjadi permukaan gambar, memungkinkan Anda untuk menggunakannya sebagai target render Direct3D.
  • SwapEffect: Atur ini ke DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL.
  • Format: Format DXGI_FORMAT_B8G8R8A8_UNORM menentukan warna 32-bit: 8 bit untuk masing-masing dari tiga saluran warna RGB, dan 8 bit untuk saluran alfa.
  • BufferCount: Atur ini ke 2 untuk perilaku buffer ganda tradisional untuk menghindari robek. Atur jumlah buffer ke 3 jika konten grafis Anda membutuhkan lebih dari satu siklus refresh monitor untuk merender satu bingkai (pada 60 Hz misalnya, ambang batas lebih dari 16 md).
  • SampleDesc: Bidang ini mengontrol multisampling. Atur Hitung ke 1 dan Kualitas ke 0 untuk rantai pertukaran model balik. (Untuk menggunakan multisampling dengan rantai pertukaran model balik, gambar target render multisampel terpisah dan kemudian selesaikan target tersebut ke rantai pertukaran tepat sebelum menyajikannya. Contoh kode disediakan dalam Multisampling di aplikasi Windows Store.)

Setelah menentukan konfigurasi untuk rantai pertukaran, Anda harus menggunakan pabrik DXGI yang sama yang membuat perangkat Direct3D (dan konteks perangkat) untuk membuat rantai pertukaran.

Formulir pendek:

Dapatkan referensi ID3D11Device yang Anda buat sebelumnya. Perkirakan ke IDXGIDevice3 (jika Anda belum melakukannya) lalu panggil IDXGIDevice::GetAdapter untuk memperoleh adaptor DXGI. Dapatkan pabrik induk untuk adaptor tersebut dengan memanggil IDXGIAdapter::GetParent (IDXGIAdapter mewarisi dari IDXGIObject)—sekarang Anda dapat menggunakan pabrik tersebut untuk membuat rantai pertukaran dengan memanggil CreateSwapChainForHwnd, seperti yang terlihat dalam sampel kode berikut.

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

Jika Anda baru memulai, mungkin yang terbaik adalah menggunakan konfigurasi yang ditampilkan di sini. Sekarang pada titik ini, jika Anda sudah terbiasa dengan versi DirectX sebelumnya, Anda mungkin bertanya: "Mengapa kami tidak membuat perangkat dan menukar rantai pada saat yang sama, alih-alih berjalan kembali melalui semua kelas tersebut?" Jawabannya adalah efisiensi: rantai pertukaran adalah sumber daya perangkat Direct3D, dan sumber daya perangkat terkait dengan perangkat Direct3D tertentu yang membuatnya. Jika Anda membuat perangkat baru dengan rantai pertukaran baru, Anda harus membuat ulang semua sumber daya perangkat Anda menggunakan perangkat Direct3D baru. Jadi dengan membuat rantai pertukaran dengan pabrik yang sama (seperti yang ditunjukkan di atas), Anda dapat membuat ulang rantai pertukaran dan terus menggunakan sumber daya perangkat Direct3D yang telah Anda muat!

Sekarang Anda memiliki jendela dari sistem operasi, cara untuk mengakses GPU dan sumber dayanya, dan rantai pertukaran untuk menampilkan hasil penyajian. Yang tersisa hanya untuk menyambungkan semuanya bersama-sama!

Membuat target render untuk menggambar

Alur shader membutuhkan sumber daya untuk menggambar piksel. Cara paling sederhana untuk membuat sumber daya ini adalah dengan menentukan sumber daya ID3D11Texture2D sebagai buffer belakang untuk digambar oleh shader piksel, lalu membaca tekstur tersebut ke dalam rantai pertukaran.

Untuk melakukan ini, Anda membuat tampilan target render. Di Direct3D, tampilan adalah cara untuk mengakses sumber daya tertentu. Dalam hal ini, tampilan memungkinkan shader piksel untuk menulis ke dalam tekstur saat menyelesaikan operasi per pikselnya.

Mari kita lihat kode untuk ini. Saat Anda mengatur DXGI_USAGE_RENDER_TARGET_OUTPUT pada rantai pertukaran, yang mengaktifkan sumber daya Direct3D yang mendasar untuk digunakan sebagai permukaan gambar. Jadi untuk mendapatkan tampilan target render kami, kita hanya perlu mendapatkan buffer belakang dari rantai pertukaran dan membuat tampilan target render yang terikat ke sumber daya buffer belakang.

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

Buat juga buffer stensil kedalaman. Buffer stensil kedalaman hanyalah bentuk tertentu dari sumber daya ID3D11Texture2D , yang biasanya digunakan untuk menentukan piksel mana yang memiliki prioritas gambar selama rasterisasi berdasarkan jarak objek di adegan dari kamera. Buffer stensil kedalaman juga dapat digunakan untuk efek stensil, di mana piksel tertentu dibuang atau diabaikan selama rasterisasi. Buffer ini harus berukuran sama dengan target render. Perhatikan bahwa Anda tidak dapat membaca dari atau merender ke tekstur stensil kedalaman buffer bingkai karena digunakan secara eksklusif oleh alur shader sebelum dan selama rasterisasi akhir.

Buat juga tampilan untuk buffer stensil kedalaman sebagai ID3D11DepthStencilView. Tampilan memberi tahu alur shader cara menginterpretasikan sumber daya ID3D11Texture2D yang mendasarinya - jadi jika Anda tidak menyediakan tampilan ini, tidak ada pengujian kedalaman per piksel yang dilakukan, dan objek dalam adegan Anda mungkin tampak sedikit di dalam paling tidak!

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

Langkah terakhir adalah membuat viewport. Ini mendefinisikan persegi panjang yang terlihat dari buffer belakang yang ditampilkan di layar; Anda dapat mengubah bagian buffer yang ditampilkan di layar dengan mengubah parameter viewport. Kode ini menargetkan seluruh ukuran jendela—atau resolusi layar, dalam kasus rantai pertukaran layar penuh. Untuk bersenang-senang, ubah nilai koordinat yang disediakan dan amati hasilnya.

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

Dan begitulah cara Anda pergi dari apa-apa untuk menggambar piksel di jendela! Saat Anda memulai, ada baiknya untuk membiasakan diri dengan bagaimana DirectX, melalui DXGI, mengelola sumber daya inti yang Anda butuhkan untuk mulai menggambar piksel.

Selanjutnya Anda akan melihat struktur alur grafis; lihat Memahami alur penyajian templat aplikasi DirectX.

Berikutnya

Bekerja dengan shader dan sumber daya shader