Freigeben über


Arbeiten mit DirectX-Geräteressourcen

Grundlegendes zur Rolle der Microsoft DirectX Graphics Infrastructure (DXGI) in Ihrem Windows Store-DirectX-Spiel. DXGI ist eine Reihe von APIs, die zum Konfigurieren und Verwalten von Grafiken und Grafikadapterressourcen auf niedriger Ebene verwendet werden. Ohne diese hätten Sie keine Möglichkeit, die Grafiken Ihres Spiels in ein Fenster zu zeichnen.

Stellen Sie sich DXGI folgendermaßen vor: Um direkt auf die GPU zuzugreifen und ihre Ressourcen zu verwalten, müssen Sie eine Möglichkeit haben, sie für Ihre App zu beschreiben. Die wichtigste Information, die Sie zur GPU benötigen, ist die Stelle zum Zeichnen von Pixeln, die dann an den Bildschirm gesendet werden können. In der Regel wird diese als „Hintergrundpuffer“ bezeichnet – ein Ort im GPU-Speicher, an dem Sie die Pixel zeichnen und dann auf ein Aktualisierungssignal hin kippen (Flip) oder austauschen (Swap) und an den Bildschirm senden lassen. Mit DXGI können Sie diesen Ort und die Mittel zum Verwenden dieses Puffers abrufen (als Swapchain bezeichnet, weil es sich um eine Kette austauschbarer Puffer handelt, die mehrere Pufferstrategien ermöglicht).

Dazu benötigen Sie Schreibzugriff auf die Swapchain und ein Handle für das Fenster, in dem der aktuelle Hintergrundpuffer für die Swapchain angezeigt wird. Außerdem müssen Sie die beiden verbinden, um sicherzustellen, dass das Betriebssystem das Fenster mit dem Inhalt des Hintergrundpuffers auf Ihre Anforderung hin aktualisiert.

Der gesamte Prozess zum Zeichnen auf den Bildschirm lautet wie folgt:

  • Rufen Sie ein CoreWindow für Ihre App ab.
  • Rufen Sie eine Schnittstelle für das Direct3D-Gerät und den Kontext ab.
  • Erstellen Sie die Swapchain, um das gerenderte Bild im CoreWindow anzuzeigen.
  • Erstellen Sie ein Renderziel zum Zeichnen, und füllen Sie es mit Pixeln auf.
  • Stellen Sie die Swapchain dar.

Erstellen eines Fensters für Ihre App

Als Erstes müssen wir ein Fenster erstellen. Erstellen Sie zunächst eine Fensterklasse, indem Sie eine Instanz von WNDCLASS auffüllen und dann mit RegisterClass registrieren. Die Fensterklasse enthält wesentliche Eigenschaften des Fensters, darunter das von ihm verwendete Symbol, die Funktion für die Verarbeitung statischer Nachrichten (mehr dazu später) und ein eindeutiger Name für die Fensterklasse.

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

Als Nächstes erstellen Sie das Fenster. Außerdem müssen wir Größeninformationen für das Fenster und den Namen der soeben erstellten Fensterklasse angeben. Wenn Sie CreateWindow aufrufen, erhalten Sie einen nicht transparenten Zeiger auf das Fenster namens HWND zurück. Sie müssen den HWND-Zeiger beibehalten und jedes Mal verwenden, wenn Sie auf das Fenster verweisen müssen, um es beispielsweise zu zerstören oder neu zu erstellen, und (besonders wichtig) beim Erstellen der DXGI-Swapchain, die Sie zum Zeichnen im Fenster verwenden.

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

Das Windows-Desktop-App-Modell enthält einen Hook in die Windows-Nachrichtenschleife. Sie müssen Ihre Hauptprogrammschleife von diesem Hook aus starten, indem Sie eine StaticWindowProc-Funktion zum Verarbeiten von Fensterereignissen schreiben. Dabei muss es sich um eine statische Funktion handeln, weil Windows sie außerhalb des Kontexts einer Klasseninstanz aufruft. Hier ist ein sehr einfaches Beispiel für eine statische Nachrichtenverarbeitungsfunktion aufgeführt.

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

In diesem einfachen Beispiel wird nur nach Bedingungen zum Beenden des Programms gesucht: WM_CLOSE wird gesendet, wenn das Fenster geschlossen werden soll, und WM_DESTROY wird gesendet, nachdem das Fenster tatsächlich vom Bildschirm entfernt wurde. Eine vollständige Produktions-App muss auch andere Fensterereignisse verarbeiten. Eine vollständige Liste der Fensterereignisse finden Sie unter Fensterbenachrichtigungen.

Die Hauptprogrammschleife selbst muss Windows-Nachrichten bestätigen, indem sie Windows die Ausführung der statischen Nachrichtenprozedur gestattet. Unterstützen Sie eine effiziente Ausführung des Programms, indem Sie das Verhalten forken: Jede Iteration soll neue Windows-Nachrichten verarbeiten, wenn sie verfügbar sind, und wenn die Warteschlange keine Nachrichten enthält, soll ein neuer Frame gerendert werden. Hier ist ein sehr einfaches Beispiel:

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

Abrufen einer Schnittstelle für das Direct3D-Gerät und den Kontext

Der erste Schritt bei der Verwendung von Direct3D besteht darin, eine Schnittstelle für die Direct3D-Hardware (GPU) abzurufen, die als Instanzen von ID3D11Device und ID3D11DeviceContext dargestellt wird. Ersteres ist eine virtuelle Darstellung der GPU-Ressourcen, und Letzteres ist eine geräteunabhängige Abstraktion von Renderingpipeline und -prozess. Das können Sie sich ganz einfach so vorstellen: ID3D11Device enthält die Grafikmethoden, die Sie selten (normalerweise vor einem Rendering) aufrufen, um die Ressourcen abzurufen und zu konfigurieren, die Sie zum Zeichnen von Pixeln benötigen. ID3D11DeviceContext enthält dagegen die Methoden, die Sie für jeden Frame aufrufen: das Laden in Puffer, Ansichten und andere Ressourcen, das Ändern des Zustands von Ausgabezusammenführung und Rasterizer, das Verwalten von Shadern sowie das Zeichnen der Ergebnisse der Weitergabe dieser Ressourcen durch die Zustände und Shader.

Ein Teil dieses Prozesses ist sehr wichtig: das Festlegen der Featureebene. Die Featureebene teilt DirectX mit, welche Mindesthardware von Ihrer App unterstützt wird. Dabei ist D3D_FEATURE_LEVEL_9_1 der niedrigste und D3D_FEATURE_LEVEL_11_1 der zurzeit höchste Featuresatz. Sie sollten mindestens 9_1 unterstützen, wenn Sie die größtmögliche Zielgruppe erreichen möchten. Nehmen Sie sich etwas Zeit, um sich über Direct3D-Featureebenen zu informieren und die minimalen und maximalen Featureebenen zu bewerten, die Ihr Spiel unterstützen soll, und um die Auswirkungen Ihrer jeweiligen Entscheidung zu verstehen.

Rufen Sie Verweise (Zeiger) sowohl auf das Direct3D-Gerät als auch auf den Gerätekontext ab, und speichern Sie sie als Variablen auf Klassenebene in der DeviceResources-Instanz (als intelligente ComPtr-Zeiger). Verwenden Sie diese Verweise immer dann, wenn Sie auf das Direct3D-Gerät oder den Gerätekontext zugreifen müssen.

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

Erstellen der Swapchain

Sie verfügen jetzt über ein Fenster, in das Sie zeichnen können, und über eine Schnittstelle zum Senden von Daten und zum Übergeben von Befehlen an die GPU. Sehen wir uns nun an, wie diese Komponenten zusammenzubringen sind.

Zunächst teilen Sie DXGI mit, welche Werte für die Eigenschaften der Swapchain verwendet werden sollen. Verwenden Sie dazu eine DXGI_SWAP_CHAIN_DESC-Struktur. Sechs Felder sind für Desktop-Apps besonders wichtig:

  • Windowed: Gibt an, ob die Swapchain den ganzen Bildschirm einnimmt oder auf das Fenster zugeschnitten ist. Legen Sie diesen Wert auf TRUE fest, um die Swapchain in dem Fenster einzufügen, das Sie zuvor erstellt haben.
  • BufferUsage: Legen Sie diesen Wert auf DXGI_USAGE_RENDER_TARGET_OUTPUT fest. Dadurch geben Sie an, dass es sich bei der Swapchain um eine Zeichnungsoberfläche handelt, sodass Sie sie als Direct3D-Renderziel verwenden können.
  • SwapEffect: Legen Sie diesen Wert auf DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL fest.
  • Format: Das DXGI_FORMAT_B8G8R8A8_UNORM-Format gibt 32-Bit-Farbe an: jeweils 8 Bits für jeden der drei RGB-Farbkanäle und 8 Bits für den Alphakanal.
  • BufferCount: Legen Sie diesen Wert für ein herkömmliches Verhalten mit doppelter Pufferung auf 2 fest, um Tearing zu vermeiden. Legen Sie die Pufferanzahl auf 3 fest, wenn der Grafikinhalt mehrere Monitoraktualisierungszyklen benötigt, um einen einzelnen Frame zu rendern (bei 60 Hz beträgt der Schwellenwert z. B. mehr als 16 ms).
  • SampleDesc: Dieses Feld steuert das Multisampling. Legen Sie Count auf 1 und Quality auf 0 für Flipmodell-Swapchains fest. (Wenn Sie Multisampling mit Flipmodell-Swapchains verwenden möchten, zeichnen Sie in ein separates Multisampling-Renderziel, und lösen Sie dieses Ziel dann direkt vor dem Darstellen in die Swapchain auf. Beispielcode wird unter Multisampling in Windows Store-Apps bereitgestellt.)

Nachdem Sie eine Konfiguration für die Swapchain angegeben haben, müssen Sie zum Erstellen der Swapchain dieselbe DXGI-Factory verwenden, die das Direct3D-Gerät (und den Gerätekontext) erstellt hat.

Kurzform:

Rufen Sie den zuvor erstellten ID3D11Device-Verweis ab. Führen Sie per Upcast eine Umwandlung in IDXGIDevice3 durch (falls noch nicht geschehen), und rufen Sie dann IDXGIDevice::GetAdapter auf, um den DXGI-Adapter zu erhalten. Rufen Sie die übergeordnete Factory für diesen Adapter ab, indem Sie IDXGIAdapter::GetParent aufrufen (IDXGIAdapter erbt von IDXGIObject). Jetzt können Sie anhand dieser Factory die Swapchain erstellen, indem Sie CreateSwapChainForHwnd aufrufen, wie im folgenden Codebeispiel gezeigt.

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

Wenn Sie gerade erst anfangen, empfiehlt es sich wahrscheinlich, die hier gezeigte Konfiguration zu verwenden. Wenn Sie bereits mit früheren Versionen von DirectX vertraut sind, fragen Sie sich möglicherweise, warum wir das Gerät und die Swapchain nicht gleichzeitig erstellt haben, statt all diese Klassen rückwärts zu durchlaufen? Die Antwort ist Effizienz: Swapchains sind Direct3D-Geräteressourcen, und Geräteressourcen sind an das jeweilige Direct3D-Gerät gebunden, das sie erstellt hat. Wenn Sie ein neues Gerät mit einer neuen Swapchain erstellen, müssen Sie alle Geräteressourcen mithilfe des neuen Direct3D-Geräts neu erstellen. Indem Sie also die Swapchain mit derselben Factory (wie oben gezeigt) erstellen, können Sie die Swapchain neu erstellen und weiterhin die Direct3D-Geräteressourcen verwenden, die Sie bereits geladen haben.

Jetzt verfügen Sie über ein Fenster vom Betriebssystem, über eine Möglichkeit für den Zugriff auf die GPU und deren Ressourcen sowie über eine Swapchain zum Anzeigen der Renderingergebnisse. Es muss nur noch alles miteinander verbunden werden!

Erstellen eines Renderziels für die Zeichnung

Die Shaderpipeline benötigt eine Ressource, in die Pixel gezeichnet werden. Die einfachste Möglichkeit zum Erstellen dieser Ressource besteht darin, eine ID3D11Texture2D-Ressource als Hintergrundpuffer für den Pixelshader zu definieren, in den gezeichnet werden soll, und diese Textur dann in die Swapchain zu lesen.

Dazu erstellen Sie eine Ansicht des Renderziels. In Direct3D ist eine Ansicht eine Möglichkeit, auf eine bestimmte Ressource zuzugreifen. In diesem Fall ermöglicht die Ansicht dem Pixelshader das Schreiben in die Textur, während er seine pixelbezogenen Vorgänge durchführt.

Sehen wir uns den Code dafür an. Als Sie DXGI_USAGE_RENDER_TARGET_OUTPUT für die Swapchain festgelegt haben, wurde dadurch die zugrunde liegende Direct3D-Ressource als Zeichnungsoberfläche aktiviert. Um unsere Renderzielansicht zu erhalten, müssen wir nur den Hintergrundpuffer aus der Swapchain abrufen und eine Renderzielansicht erstellen, die an die Hintergrundpufferressource gebunden ist.

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

Erstellen Sie auch einen Tiefenschablonenpuffer. Ein Tiefenschablonenpuffer ist nur eine bestimmte Form der ID3D11Texture2D-Ressource, mit der in der Regel festgelegt wird, welche Pixel während der Rasterung Vorrang haben. Diese Festlegung basiert auf dem Abstand der Objekte in der Szene von der Kamera. Ein Tiefenschablonenpuffer kann auch für Schabloneneffekte verwendet werden, wobei bestimmte Pixel während der Rasterung verworfen oder ignoriert werden. Dieser Puffer muss dieselbe Größe wie das Renderziel aufweisen. Beachten Sie, dass Sie die Tiefenschablonentextur des Framepuffers nicht lesen oder rendern können, da sie ausschließlich von der Shaderpipeline vor und während der endgültigen Rasterung verwendet wird.

Erstellen Sie außerdem eine Ansicht für den Tiefenschablonenpuffer als ID3D11DepthStencilView. Die Ansicht teilt der Shaderpipeline mit, wie die zugrunde liegende ID3D11Texture2D-Ressource interpretiert werden soll. Wenn Sie diese Ansicht also nicht bereitstellen, werden keine pixelbezogenen Tiefentests durchgeführt, und die Objekte in Ihrer Szene erscheinen zumindest ein wenig verdreht.

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

Der letzte Schritt besteht darin, einen Viewport zu erstellen. Dadurch wird das sichtbare Rechteck des Hintergrundpuffers definiert, der auf dem Bildschirm angezeigt wird. Sie können den Teil des Puffers ändern, der auf dem Bildschirm angezeigt wird, indem Sie die Parameter des Viewports ändern. Dieser Code zielt auf die gesamte Fenstergröße oder – im Fall von Swapchains im Vollbildmodus – auf die Bildschirmauflösung ab. Ändern Sie die angegebenen Koordinatenwerte, und beobachten Sie die Ergebnisse.

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

Jetzt können Sie schon Pixel in ein Fenster zeichnen! Zu Beginn sollten Sie sich damit vertraut machen, wie DirectX über DXGI die wichtigsten Ressourcen verwaltet, die Sie zum Zeichnen von Pixeln benötigen.

Als Nächstes sehen Sie sich die Struktur der Grafikpipeline an. Informationen hierzu finden Sie unter Grundlegendes zur Renderingpipeline der DirectX-App-Vorlage.

Als Nächstes

Arbeiten mit Shadern und Shaderressourcen