Direct3D12でフルスクリーンに切り替える

ChatGPT先生に聞きながら、Direct3D12でフルスクリーンモードとウィンドウモードの切り替えを実装しました。 F11キーを押すことで切り替えるというものです。

SampleApp g_app;

extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    // ImGuiのウィンドウ処理
    if (ImGui_ImplWin32_WndProcHandler(hWnd, message, wParam, lParam)) {
        return true;
    }

    static bool isFullscreen = false;
    static bool isFullscreenInActive = false;
    static WINDOWPLACEMENT windowPlacement = { sizeof(WINDOWPLACEMENT) }; // ウィンドウモード時の位置とサイズを保存

    switch (message) {
    case WM_DESTROY:
        g_app.SetFullscreenState(FALSE);
        PostQuitMessage(0);
        return 0;
    case WM_KEYDOWN:
        if (wParam == VK_F11) { // F11キーで切り替え
            if (isFullscreen) {
                // ウィンドウモードに戻す
                g_app.SetFullscreenState(FALSE);
                SetWindowLong(hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
                SetWindowPlacement(hWnd, &windowPlacement); // 以前のウィンドウの位置とサイズを復元
                SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
                isFullscreen = false;
            }
            else {
                // フルスクリーンモードにする
                GetWindowPlacement(hWnd, &windowPlacement); // 現在のウィンドウの位置とサイズを保存
                SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE); // ウィンドウの境界線とタイトルバーを削除
                SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
                //SetWindowPos(hWnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_FRAMECHANGED);
                g_app.SetFullscreenState(TRUE);
                isFullscreen = true;
            }
        }
        return 0;
    case WM_SIZE:
        if (wParam != SIZE_MINIMIZED) {
            UINT width = LOWORD(lParam);
            UINT height = HIWORD(lParam);

            g_app.SetScreenSize(width, height);
        }
        break;
    case WM_ACTIVATE:
        if (wParam == WA_INACTIVE) {
            // ウィンドウが非アクティブになった場合
            if (isFullscreen) {
                // フルスクリーンモードからウィンドウモードに戻す
                g_app.SetFullscreenState(FALSE);
                SetWindowLong(hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
                SetWindowPlacement(hWnd, &windowPlacement); // 以前の位置とサイズを復元
                SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
                isFullscreenInActive = true;
            }
        }
        else if (wParam == WA_ACTIVE || wParam == WA_CLICKACTIVE) {
            // ウィンドウが再度アクティブになった場合
            if (isFullscreenInActive) {
                // 必要ならフルスクリーンに戻す
                SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
                SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
                g_app.SetFullscreenState(TRUE);
                isFullscreenInActive = false;
            }
        }
        return 0;
    default:
        break;
    }

    return DefWindowProc(hWnd, message, wParam, lParam);
}

g_app.SetFullscreenState()は以下のような定義です。

    bool SetFullscreenState(BOOL is_fullscreen)
    {
        fullScreen = (is_fullscreen == TRUE);
        if (swapChain)
        {
            // コマンドキューのフラッシュ処理
            WaitForFence();

            // フルスクリーンの設定
            swapChain->SetFullscreenState(is_fullscreen, nullptr);
            return true;
        }
        return false;
    }

WM_ACTIVATEの処理は必要なのかよくわかりませんが、 Alt+Tab などでフルスクリーン状態のアプリがアクティブでなくなったとき、一時的にウィンドウモードに戻すという処理を書いています。

はまった点は、この部分です。

SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
//SetWindowPos(hWnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_FRAMECHANGED);

フルスクリーンにするときに、モニターの解像度を設定しようとしたのですが、サイズは設定しないというのが正解でした。ここで設定しなくても、 SetFullscreenStateのタイミングでサイズ変更の処理が自動的に呼ばれます。

ついでに、SetScreenSizeは、以下のような処理です。

    void SetScreenSize(int width, int height)
    {
        viewport.Width = static_cast<float>(width);
        viewport.Height = static_cast<float>(height);
        scissorRect.right = width;
        scissorRect.bottom = height;

        if (swapChain) {
            // コマンドキューのフラッシュ処理
            WaitForFence();

            // バックバッファをリサイズ
            for (UINT i = 0; i < FRAME_BUFFER_COUNT; ++i) {
                renderTargets[i].Reset();
            }

            // スワップチェーンのバッファをリサイズ
            swapChain->ResizeBuffers(FRAME_BUFFER_COUNT, width, height, DXGI_FORMAT_R8G8B8A8_UNORM, 0);

            // ディスクリプタヒープの再作成
            D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
            rtvHeapDesc.NumDescriptors = FRAME_BUFFER_COUNT;
            rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
            rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
            device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap));

            // 新しいバックバッファに対するRTVの再作成
            UINT rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
            D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = rtvHeap->GetCPUDescriptorHandleForHeapStart();
            for (UINT i = 0; i < FRAME_BUFFER_COUNT; ++i) {
                swapChain->GetBuffer(i, IID_PPV_ARGS(&renderTargets[i]));
                device->CreateRenderTargetView(renderTargets[i].Get(), nullptr, rtvHandle);
                rtvHandle.ptr += rtvDescriptorSize;
            }
        }
    }