Поделиться через


Работа с ресурсами устройства DirectX

Узнайте о роли инфраструктуры графики Microsoft DirectX (DXGI) в игре DirectX Магазина Windows. DXGI — это набор API, используемых для настройки ресурсов графических и графических адаптеров низкого уровня и управления ими. Без этого вы не можете нарисовать графику игры в окно.

Подумайте об DXGI таким образом: чтобы напрямую получить доступ к GPU и управлять его ресурсами, необходимо описать его в приложении. Наиболее важным элементом информации, необходимой для GPU, является место для рисования пикселей, чтобы он смог отправить эти пиксели на экран. Обычно это называется "обратным буфером" — расположением в памяти GPU, где можно рисовать пиксели, а затем перевернутые или "переключились" и отправляются на экран на сигнал обновления. DXGI позволяет получить это расположение и средства для использования этого буфера (так как это цепочка буферов, так как это цепочка буферов, допускающих переключение, что позволяет использовать несколько стратегий буферизации).

Для этого требуется доступ к записи в цепочку буферов и дескриптор в окно, отображающий текущий буфер обратной цепочки. Кроме того, необходимо подключить эти два элемента, чтобы убедиться, что операционная система обновит окно с содержимым заднего буфера при запросе на это.

Общий процесс рисования на экране выглядит следующим образом:

  • Получите CoreWindow для приложения.
  • Получите интерфейс для устройства Direct3D и контекста.
  • Создайте цепочку буферов для отображения отрисованного изображения в CoreWindow.
  • Создайте целевой объект отрисовки для рисования и заполните его пикселями.
  • Представить цепочку буферов!

Создание окна для приложения

Первое, что нужно сделать, — создать окно. Сначала создайте класс окна, заполняя экземпляр WNDCLASS, а затем зарегистрируйте его с помощью RegisterClass. Класс окна содержит основные свойства окна, включая значок, который он использует, статическую функцию обработки сообщений (подробнее об этом позже) и уникальное имя класса окна.

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

Затем вы создадите окно. Кроме того, необходимо указать сведения о размере окна и имя только что созданного класса окна. При вызове CreateWindow вы получите непрозрачный указатель на окно, называемое HWND; вам потребуется сохранить указатель HWND и использовать его в любое время, когда нужно ссылаться на окно, включая уничтожение или воссоздание его, и (особенно важно) при создании цепочки буферов DXGI, используемой для рисования в окне.

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

Модель классического приложения Windows включает перехватчик в цикл сообщений Windows. Вам потребуется создать основной цикл программы из этого перехватчика, написав функцию StaticWindowProc для обработки событий окна. Это должна быть статическую функцию, так как Windows вызовет ее вне контекста любого экземпляра класса. Ниже приведен очень простой пример функции обработки статических сообщений.

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

Этот простой пример проверка только для условий выхода программы: WM_CLOSE, отправляемый, когда окно запрашивается, чтобы быть закрытым, и WM_DESTROY, который отправляется после того, как окно фактически удаляется с экрана. Полное рабочее приложение также должно обрабатывать другие события окна— полный список событий окна см. в разделе "Уведомления о окне".

Основной цикл программы должен подтвердить сообщения Windows, позволяя Windows запускать статический прок. Помогите программе эффективно работать, вилируя поведение: каждая итерация должна обрабатывать новые сообщения Windows, если они доступны, и если сообщения не находятся в очереди, он должен отображать новый кадр. Ниже приведен очень простой пример:

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

Получение интерфейса для устройства Direct3D и контекста

Первым шагом к использованию Direct3D является получение интерфейса для оборудования Direct3D (GPU), представленного как экземпляры ID3D11Device и ID3D11DeviceContext. Первый — это виртуальное представление ресурсов GPU, а последнее — не зависящая от устройства абстракция конвейера отрисовки и процесса. Вот простой способ подумать об этом: ID3D11Device содержит методы графики, которые вы вызываете редко, как правило, до появления любой отрисовки, чтобы получить и настроить набор ресурсов, необходимых для начала рисования пикселей. Id3D11DeviceContext, с другой стороны, содержит методы, которые вы вызываете каждый кадр: загрузка в буферах и представлениях и других ресурсах, изменение состояния слияния вывода и растеризатора, управление шейдерами и рисование результатов передачи этих ресурсов через состояния и шейдеры.

Существует одна очень важная часть этого процесса: настройка уровня компонентов. Уровень компонентов сообщает DirectX минимальному уровню оборудования, которое поддерживает приложение, с D3D_FEATURE_LEVEL_9_1 как самый низкий набор функций и D3D_FEATURE_LEVEL_11_1 как текущий самый высокий. Вы должны поддерживать 9_1 как минимум, если вы хотите достичь максимально возможной аудитории. Некоторое время, чтобы прочитать на уровнях функций Direct3D и оценить для себя минимальные и максимальные уровни функций, которые вы хотите, чтобы ваша игра поддерживала и понимала последствия вашего выбора.

Получите ссылки (указатели) на контекст устройства Direct3D и устройства и сохраните их в виде переменных уровня класса в экземпляре DeviceResources (как смарт-указатели ComPtr ). Используйте эти ссылки всякий раз, когда необходимо получить доступ к контексту устройства Или устройства 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);

Создание цепочки буферов

Хорошо: у вас есть окно для рисования, и у вас есть интерфейс для отправки данных и предоставления команд GPU. Теперь давайте посмотрим, как объединить их вместе.

Во-первых, вы сообщаете DXGI, какие значения следует использовать для свойств цепочки буферов. Это можно сделать с помощью структуры DXGI_SWAP_CHAIN_DESC. Шесть полей особенно важны для классических приложений:

  • Окно: указывает, является ли цепочка буферов полноэкранной или обрезана в окно. Задайте значение TRUE, чтобы поместить цепочку буферов в созданное ранее окно.
  • BufferUsage: задайте для этого значение DXGI_USAGE_RENDER_TARGET_OUTPUT. Это означает, что цепочка буферов будет областью рисования, что позволяет использовать ее в качестве целевого объекта отрисовки Direct3D.
  • SwapEffect: задайте для этого значение DXGI_SWAP_EFFECT_Fпакет интерфейса пользователя_SEQUENTIAL.
  • Формат: формат DXGI_FORMAT_B8G8R8A8_UNORM задает 32-разрядный цвет: 8 бит для каждого из трех каналов цветов RGB и 8 бит для альфа-канала.
  • BufferCount: задайте значение 2 для традиционного двойного буферизованного поведения, чтобы избежать разрыва. Задайте для количества буферов значение 3, если графическое содержимое занимает несколько циклов обновления монитора для отрисовки одного кадра (например, при 60 Гц пороговое значение превышает 16 мс).
  • SampleDesc: это поле управляет многосамплингом. Задайте значение count 1 и Quality to 0 для цепочек буферов модели переключения с помощью перевернутой модели. (Чтобы использовать многофакторную фильтрацию с цепочками буферов с помощью модели переключения, нарисуйте отдельный многофакторный целевой объект отрисовки, а затем устраните эту цель в цепочке буферов, прежде чем представить ее. Пример кода предоставляется в многосамплинге в приложениях Магазина Windows.)

После указания конфигурации для цепочки буферов необходимо использовать ту же фабрику DXGI, которая создала устройство Direct3D (и контекст устройства), чтобы создать цепочку буферов.

Короткая форма:

Получите созданную ранее ссылку ID3D11Device . Переадресуйте его в IDXGIDevice3 (если вы еще не сделали), а затем вызовите IDXGIDevice::GetAdapter для получения адаптера DXGI. Получите родительскую фабрику для этого адаптера путем вызова IDXGIAdapter::GetParent (IDXGIAdapter наследуется от IDXGIObject)— теперь можно использовать эту фабрику для создания цепочки буферов путем вызова CreateSwapChainForHwnd, как показано в следующем примере кода.

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

Если вы только начинаете, это, вероятно, лучше всего использовать конфигурацию, показанную здесь. Теперь на этом этапе, если вы уже знакомы с предыдущими версиями DirectX, вы можете спросить: "Почему мы не создадим устройство и цепочку переключений одновременно, вместо того, чтобы вернуться ко всем этим классам?" Ответ заключается в эффективности: цепочки буферов являются ресурсами устройства Direct3D, а ресурсы устройств привязаны к конкретному созданному устройству Direct3D. При создании нового устройства с новой цепочкой буферов необходимо повторно создать все ресурсы устройства с помощью нового устройства Direct3D. Таким образом, создав цепочку буферов с той же фабрикой (как показано выше), вы можете воссоздать цепочку буферов и продолжить использование ресурсов устройств Direct3D, которые вы уже загрузили!

Теперь у вас есть окно из операционной системы, способ доступа к GPU и его ресурсам, а также цепочка буферов для отображения результатов отрисовки. Все, что осталось, заключается в том, чтобы провести всю вещь вместе!

Создание целевого объекта отрисовки для рисования

Конвейер шейдера должен иметь ресурс для рисования пикселей. Самый простой способ создания этого ресурса — определить ресурс ID3D11Texture2D в качестве обратного буфера для рисования шейдера пикселей, а затем считывать эту текстуру в цепочку буферов.

Для этого создайте представление целевого объекта отрисовки. В Direct3D представление — это способ доступа к конкретному ресурсу. В этом случае представление позволяет шейдеру пикселей записывать данные в текстуру по мере завершения операций на пиксель.

Давайте рассмотрим код для этого. При настройке DXGI_USAGE_RENDER_TARGET_OUTPUT в цепочке буферов, которая включила базовый ресурс Direct3D, используемый в качестве поверхности рисования. Таким образом, чтобы получить представление целевого объекта отрисовки, необходимо просто получить буфер обратно из цепочки буферов и создать целевое представление отрисовки, привязанное к ресурсу обратного буфера.

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

Кроме того, создайте буфер элементов глубины. Буфер элементов глубины — это только определенная форма ресурса ID3D11Texture2D , который обычно используется для определения приоритета рисования пикселей во время растеризации на основе расстояния объектов в сцене от камеры. Буфер элементов глубины также можно использовать для эффектов набора элементов, где определенные пиксели не карта или игнорируются во время растеризации. Этот буфер должен иметь тот же размер, что и целевой объект отрисовки. Обратите внимание, что вы не можете считывать из буфера буфера кадра текстуру элементов глубины, так как она используется исключительно конвейером шейдера до и во время окончательной растеризации.

Кроме того, создайте представление для буфера элементов глубины в виде ID3D11DepthStencilView. Представление сообщает конвейеру шейдера, как интерпретировать базовый ресурс ID3D11Texture2D . Поэтому если вы не предоставляете это представление без проверки глубины пикселей, а объекты в сцене могут показаться немного внутри по крайней мере!

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

Последним шагом является создание окна просмотра. Это определяет видимый прямоугольник заднего буфера, отображаемого на экране; Вы можете изменить часть буфера, отображаемую на экране, изменив параметры окна просмотра. Этот код предназначен для всего размера окна или разрешения экрана в случае цепочек буферов полноэкранного экрана. Для удовольствия измените указанные значения координат и просмотрите результаты.

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

И вот как вы идете от ничего до рисования пикселей в окне! Как вы начинаете, рекомендуется ознакомиться с тем, как DirectX, через DXGI, управляет основными ресурсами, которые необходимо начать рисование пикселей.

Далее вы увидите структуру графического конвейера; См. сведения о конвейере отрисовки шаблона приложения DirectX.

Далее

Работа с шейдерами и ресурсами шейдеров