使用 DirectX 设备资源

了解 Windows 应用商店 DirectX 游戏中 Microsoft DirectX 图形基础设施 (DXGI) 的作用。 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 消息,则每次迭代应选择处理新的 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) 的接口,这些接口表示为 ID3D11DeviceID3D11DeviceContext 的实例。 前者是 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 结构执行此操作。 以下六个字段对于桌面应用尤其重要:

  • Windowed:指示交换链是全屏还是根据窗口剪裁。 将此项设置为 TRUE 可将交换链置于之前创建的窗口中。
  • BufferUsage:将此字段设置为 DXGI_USAGE_RENDER_TARGET_OUTPUT。 这表示交换链将是绘图图面,允许将其用作 Direct3D 呈现器目标。
  • SwapEffect:将此字段设置为 DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL
  • Format:DXGI_FORMAT_B8G8R8A8_UNORM 格式指定 32 位颜色:三个 RGB 颜色通道中的每一个 8 位,alpha 通道 8 位
  • BufferCount:对于传统双缓冲行为,将此字段设置为 2,以避免撕裂。 如果图形内容需要多个监视器刷新周期来呈现单个帧(例如,频率为 60 Hz,阈值超过 16 毫秒),请将缓冲区计数设置为 3。
  • SampleDesc:此字段控制多重采样。 将翻转模型交换链的计数设置为 1,将质量设置为 0。 (若要对翻转模型交换链使用多重采样,请在单独的多重采样呈现目标上绘制,然后在演示之前将该目标解析为交换链。Windows 应用商店应用中的多重采样中提供了示例代码。)

为交换链指定配置后,必须使用创建 Direct3D 设备(和设备上下文)的同一 DXGI 工厂来创建交换链。

缩写形式:

获取之前创建的 ID3D11Device 引用。 将其向上转换为 IDXGIDevice3(如果尚未这样做),然后调用 IDXGIDevice::GetAdapter 来获取 DXGI 适配器。 通过调用 IDXGIAdapter::GetParentIDXGIAdapter 继承自 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 应用模板的呈现管道

后续操作

使用着色器和着色器资源