DXGI Alt+Enter behaves inconsistently

Started by
5 comments, last by tonemgub 9 years ago

Hey guys,

I've been trying to get fullscreen working for at least over 2 days now and the documentation does not mention all the annoying corner-cases. For example when I try to switch to fullscreen manually, I call IDXGISwapChain::ResizeTarget (with a valid mode!) and then IDXGISwapChain::SetFullscreenState. MSDN says that this is the correct way, beacause WM_SIZE will be sent which resizes the backbuffer. This seems to work for some modes, but not for all of them. For 1280x720 this works but with 800x600 I get that DXGI performance warning. Another weird corner-case is when the window has the size of a valid mode, like 1280x720, and you then switch to fullscreen via Alt+Enter. This works correctly the first time, but if you repeat this it gives you the performance warning again ...

I attached a minimalistic program, which replicates the Alt+Enter issue. Please somebody tell me what is going wrong here, I'm going crazy.

PS: I read all relevant threads here on the forum about this topic, but I can't find a solution.


#ifndef UNICODE
#define UNICODE
#endif

#include <Windows.h>
#include <d3d11.h>
#include <sstream>

#pragma comment(lib, "d3d11.lib")

namespace
{
    bool isRunning = true;
    HWND window = nullptr;
    ID3D11Device* device = nullptr;
    ID3D11DeviceContext* context = nullptr;
    IDXGISwapChain* swapChain = nullptr;
    ID3D11RenderTargetView* renderTargetView = nullptr;
}

void CreateBufferViews()
{
    ID3D11Texture2D* backBuffer;
    swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer));
    device->CreateRenderTargetView(backBuffer, nullptr, &renderTargetView);
    backBuffer->Release();
}

void ResizeBuffers()
{
    context->ClearState();
    context->Flush();

    if (renderTargetView)
    {
        renderTargetView->Release();
        renderTargetView = nullptr;
    }

    swapChain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_R8G8B8A8_UNORM,
        DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH);

    CreateBufferViews();
}

LRESULT CALLBACK WindowCallback(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CLOSE:
        DestroyWindow(window);
        break;

    case WM_DESTROY:
        isRunning = false;
        break;

    case WM_SIZE:
    {
        if (wParam != SIZE_MINIMIZED && swapChain)
        {
            ResizeBuffers();
        }
        break;
    }

    case WM_KEYDOWN:
    case WM_SYSKEYDOWN:
    {
        if (wParam == VK_ESCAPE)
        {
            isRunning = false;
        }
        break;
    }
        
    default:
        return DefWindowProcW(window, message, wParam, lParam);
    }

    return 0;
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int cmdShow)
{
    WNDCLASSW windowClass{ };
    windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
    windowClass.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
    windowClass.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
    windowClass.hInstance = hInstance;
    windowClass.lpfnWndProc = &WindowCallback;
    windowClass.lpszClassName = L"SandboxWindowClass";
    windowClass.style = CS_HREDRAW | CS_VREDRAW;
    RegisterClassW(&windowClass);

    window = CreateWindowW(L"SandboxWindowClass", L"Sandbox",
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
        1280, 720, nullptr, nullptr, hInstance, nullptr);
    ShowWindow(window, cmdShow);

    D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_DEBUG,
        nullptr, 0, D3D11_SDK_VERSION, &device, nullptr, &context);

    IDXGIDevice1* dxgiDevice;
    device->QueryInterface(IID_PPV_ARGS(&dxgiDevice));

    IDXGIAdapter1* adapter;
    dxgiDevice->GetParent(IID_PPV_ARGS(&adapter));

    IDXGIFactory1* factory;
    adapter->GetParent(IID_PPV_ARGS(&factory));

    RECT clientRect;
    GetClientRect(window, &clientRect);

    DXGI_SWAP_CHAIN_DESC swapChainDesc{ };
    swapChainDesc.BufferCount = 1;
    swapChainDesc.BufferDesc.Width = clientRect.right;
    swapChainDesc.BufferDesc.Height = clientRect.bottom;
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
    swapChainDesc.OutputWindow = window;
    swapChainDesc.SampleDesc.Count = 1;
    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    swapChainDesc.Windowed = true;

    factory->CreateSwapChain(device, &swapChainDesc, &swapChain);
    factory->MakeWindowAssociation(window, 0);

    factory->Release();
    adapter->Release();
    dxgiDevice->Release();

    CreateBufferViews();

    MSG message;
    while (isRunning)
    {
        if (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&message);
            DispatchMessageW(&message);
        }
        else
        {
            static const float color[] = { 1, 0, 1, 1 };
            context->ClearRenderTargetView(renderTargetView, color);
            context->OMSetRenderTargets(1, &renderTargetView, nullptr);

            swapChain->Present(1, 0);
        }
    }

    swapChain->SetFullscreenState(false, nullptr);
    context->ClearState();
    context->Flush();

    renderTargetView->Release();
    swapChain->Release();
    context->Release();
    device->Release();

    UnregisterClassW(L"SandboxWindowClass", hInstance);
    return 0;
}
Advertisement

This was discussed before. To avoid that warning, before you call ResizeBuffers, the client size of the window attached to the D3D device must match the size of the fullscreen resolution.

The warning happens because ResizeTarget will not change the window size in fullscreen mode (or IIRC ResizeTarget does set the window size correctly, but then SetFullscreenState also resizes the entire window to fill the screen, so the client area will not match the fullscreen size), but ResizeBuffers will still use the window's client size if you pass 0 for the Width and Height parameters.

For manual switching, if you add another call to ResizeTarget right after SetFullscreenState (but leave the other call in there as well), you shouldn't get the warning anymore.

But for the default Alt+Enter handler, you can't do this, so I think you must instead pass the proper width and height to ResizeBuffers, instead of 0.

Also, make sure that when you re-create the depth-stencil buffer, you use the width and height of the backbuffer ( GetBuffer(0) ), not the width and height of the window client (or the values from WM_SIZE's lParam). If you don't, you will get another warning.

Yes you were right, I need to call ResizeBuffers effectively twice when going fullscreen manually. The first time before calling SetFullscreenState, because DXGI chooses the display mode by looking at the backbuffer size and format. And then for some reason we need to call ResizeBuffers again with the same size so DXGI stops printing the warning. I finally got everything to work by checking if we need to resize or the fullscreen state changed every frame. Seems like a ugly hack since MSDN's guidelines don't work for me...


#ifndef UNICODE
#define UNICODE
#endif

#include <Windows.h>
#include <d3d11.h>
#include <sstream>

#pragma comment(lib, "d3d11.lib")

namespace
{
    bool isRunning = true;
    HWND window = nullptr;
    ID3D11Device* device = nullptr;
    ID3D11DeviceContext* context = nullptr;
    IDXGISwapChain* swapChain = nullptr;
    ID3D11RenderTargetView* renderTargetView = nullptr;
    
    // Fullscreen stuff
    bool needsResize = false;
    BOOL isFullscreen = FALSE;
}

void CreateBufferViews()
{
    ID3D11Texture2D* backBuffer;
    swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer));
    device->CreateRenderTargetView(backBuffer, nullptr, &renderTargetView);
    backBuffer->Release();
}

void ResizeBuffers()
{
    context->ClearState();
    context->Flush();
    
    if (renderTargetView)
    {
        renderTargetView->Release();
        renderTargetView = nullptr;
    }

    swapChain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_UNKNOWN,
        DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH);

    CreateBufferViews();
}

void SetFullscreen(UINT width, UINT height, bool fullscreen)
{
    DXGI_MODE_DESC mode{};
    mode.Width = width;
    mode.Height = height;
    mode.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

    if (fullscreen)
    {
        IDXGIOutput* output;
        swapChain->GetContainingOutput(&output);
        
        DXGI_MODE_DESC closestMatch;
        output->FindClosestMatchingMode(&mode, &closestMatch, device);
        output->Release();

        swapChain->ResizeTarget(&closestMatch);
        ResizeBuffers(); // MSDN says DXGI chooses mode by taking backbuffer params, so resize the backbuffer :)
        swapChain->SetFullscreenState(true, nullptr);
    }
    else
    {
        swapChain->SetFullscreenState(false, nullptr);
        swapChain->ResizeTarget(&mode); // We dont want a mode switch so resize window after going to windowed
    }
}

LRESULT CALLBACK WindowCallback(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CLOSE:
        DestroyWindow(window);
        break;

    case WM_DESTROY:
        isRunning = false;
        break;

    case WM_SIZE:
    {
        needsResize = true;
        break;
    }

    case WM_KEYDOWN:
    case WM_SYSKEYDOWN:
    {
        if (wParam == VK_ESCAPE)
        {
            isRunning = false;
        }
        else if (wParam == VK_SPACE) // For testing manual switching
        {
            if (isFullscreen)
            {
                SetFullscreen(1280, 720, false);
            }
            else
            {
                SetFullscreen(1920, 1080, true);
            }
        }
        break;
    }
        
    default:
        return DefWindowProcW(window, message, wParam, lParam);
    }

    return 0;
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int cmdShow)
{
    WNDCLASSW windowClass{ };
    windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
    windowClass.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
    windowClass.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
    windowClass.hInstance = hInstance;
    windowClass.lpfnWndProc = &WindowCallback;
    windowClass.lpszClassName = L"SandboxWindowClass";
    windowClass.style = CS_HREDRAW | CS_VREDRAW;
    RegisterClassW(&windowClass);

    RECT windowRect = { 0, 0, 1280, 720 };
    AdjustWindowRect(&windowRect, WS_OVERLAPPEDWINDOW, false);
    auto windowWidth = windowRect.right - windowRect.left;
    auto windowHeight = windowRect.bottom - windowRect.top;

    window = CreateWindowW(L"SandboxWindowClass", L"Sandbox",
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
        windowWidth, windowHeight, nullptr, nullptr, hInstance, nullptr);
    ShowWindow(window, cmdShow);

    D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_DEBUG,
        nullptr, 0, D3D11_SDK_VERSION, &device, nullptr, &context);

    IDXGIDevice1* dxgiDevice;
    device->QueryInterface(IID_PPV_ARGS(&dxgiDevice));

    IDXGIAdapter1* adapter;
    dxgiDevice->GetParent(IID_PPV_ARGS(&adapter));

    IDXGIFactory1* factory;
    adapter->GetParent(IID_PPV_ARGS(&factory));

    RECT clientRect;
    GetClientRect(window, &clientRect);
    UINT backBufferWidth = static_cast<UINT>(clientRect.right - clientRect.left);
    UINT backBufferHeight = static_cast<UINT>(clientRect.bottom - clientRect.top);

    DXGI_SWAP_CHAIN_DESC swapChainDesc{ };
    swapChainDesc.BufferCount = 1;
    swapChainDesc.BufferDesc.Width = backBufferWidth;
    swapChainDesc.BufferDesc.Height = backBufferHeight;
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
    swapChainDesc.OutputWindow = window;
    swapChainDesc.SampleDesc.Count = 1;
    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    swapChainDesc.Windowed = true;

    factory->CreateSwapChain(device, &swapChainDesc, &swapChain);

    factory->Release();
    adapter->Release();
    dxgiDevice->Release();
    
    CreateBufferViews();

    MSG message;
    while (isRunning)
    {
        if (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&message);
            DispatchMessageW(&message);
        }
        else
        {
            // BEGIN (annoying fullscreen stuff)
            BOOL fullscreenState;
            swapChain->GetFullscreenState(&fullscreenState, nullptr);

            if (needsResize || isFullscreen != fullscreenState)
            {
                ResizeBuffers();
                needsResize = false;
            }

            isFullscreen = fullscreenState;
            // END (annoying fullscreen stuff)

            static const float color[] = { 1, 0, 1, 1 };
            context->ClearRenderTargetView(renderTargetView, color);
            context->OMSetRenderTargets(1, &renderTargetView, nullptr);

            swapChain->Present(1, 0);
        }
    }

    swapChain->SetFullscreenState(false, nullptr);
    context->ClearState();
    context->Flush();

    renderTargetView->Release();
    swapChain->Release();
    context->Release();
    device->Release();

    UnregisterClassW(L"SandboxWindowClass", hInstance);
    return 0;
}

MSDN's guidelines don't work for me...

I don't remember on what MSDN page I saw this exactly (IIRC, it had some example code too), but MSDN seems to imply that the Alt+Enter handler works on the assumption that you always constrain your window's client size to one of the resolutions supported by the adapter, by handling WM_WINDOWPOSCHANGING (or WM_SIZING). If you've noticed, most video games that let you switch between windowed and fullscreen also do this. This is also what IDXGIOutput::FindClosestMatchingMode is intended to be used for.

Unfortunately this also means that if you want to have arbitrary window sizes in windowed mode and no performance loss in fullscreen (as the warning says), you have to disable the default Alt+Enter handler and implement your own, where you specify the sizes and resolutions you want during switching and resizing.

But if you ask me, this is just the work of a lazy programmer at Microsoft, who went so far as to implement the Alt+enter handler but could not be bothered to also implement the related WM_WINDOWPOSCHANGING handling, and instead they added that warning. smile.png

I've got this working cleanly before, and I'll need to go over some old code to confirm, but my recollection is that (and despite the MSDN documentation) the order in which you make your ResizeTarget and SetFullscreenState calls is significant depending on whether you're going fullscreen to windowed or windowed to fullscreen. It's well-behaved to make the ResizeBuffers call after the mode transition completes.

I didn't handle it directly from windows messages themselves, but instead for each relevant message I just set a dirty flag, and at the start of each frame I check for dirty flags and respond accordingly. The end result was quite clean and integrated well and consistently with changes coming from windows messages or menu options. Alt-Enter didn't need any special handling (aside from detecting when it happened) using this approach.

My experience overall with DXGI was that if you're happy for it to handle everything for you, it will, but you'll lost the ability to use custom fullscreen modes and maybe one or two other things; on the other hand if you need that ability then you have to do everything yourself, and it's worse than D3D9 was if so. There is no middle-ground.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

Yes you guys are right, after all my testing it seems that the default DXGI handler for Alt+Enter does not like sizes that are not valid display modes. I looked at some commercial games and they all do not permit resizing the window by dragging. Which of course makes sense. I guess the default behavior of DXGI is fine for all game scenarios, because you would only want valid display modes anyway and resizing the window wouldn't make much sense. And for editor viewports I don't see a reason to go fullscreen.

But I would still like to see a better documentation on this topic. The information provided is scattered all over MSDN which makes it very time consuming to find everything and they do not mention a lot of things (at least not explicitly) ...

The funny thing is that, when the window's client size is valid, DXGI outputs the warning again. So the default handler for Alt+Enter does not handle other display modes other than the native one very well. The only fix for that is to check every frame if the fullscreen state has changed and respond to that (like in post #3). Or just don't allow mode changes, then everything works if you handle WM_SIZE correctly. The last option is ofc to just do everything on your own.


But I would still like to see a better documentation on this topic. The information provided is scattered all over MSDN which makes it very time consuming to find everything and they do not mention a lot of things (at least not explicitly) ...

Welcome to the wonderful, wonderful world of MSDN. smile.png

Really though, all you can do with MSDN is take what you get. If there was concrete documentation for this somewhere, we would be throwing it around, but IIRC there isn't. When I dealt with it, I don't remember even finding this explanation in non-MSDN sources. I figured it out myself by trial and error - and trust me, there was a whole lot of trial and error. The ball dropped only when I realized what other games were doing. And like I said before, there is also a brief mention that you might have to constrain the window size like that somewhere on a MSDN page with lots of code examples, but it's so obscure you'd probably miss it if you blink. I'm not even going to bother looking for it, for all the headaches it gave me.

Let's hope someone from Microsoft trolls these forums and they will add this to the documentation somewhere, possibly in bold, capital letters and a huge font size. smile.png


The funny thing is that, when the window's client size is valid, DXGI outputs the warning again.

I already explained why this happens (maybe I edited one of my posts after you read it). After changing the resolution, SetFullscreenState(true) also resizes the window to cover the whole screen - the whole window, not just the client area! So when you handle the WM_SIZE generated by this SetFullscreenState window resize (and try to resize the buffers), the client area is not really the same as the screen resolution. If ResizeBuffers works properly the next frame, probably by then the window's client size is changed again, and there should be another WM_SIZE message? Or maybe you are no longer passing 0 for width and height to ResizeBuffers?

Either that, or the SetFullscreenState call only really takes effect during Present? Although I do seem to remember specifically that the client size in fullscreen mode didn't match the screen resolution. Weird, I know. At first I thought maybe some third party software was overriding the window placement, but I also tested it on a clean install in VirtualBox, (used the WARP driver, IIRC), and the same happened. Anyway, I'm just glad I finally did find a solution, and I'm too lazy to remember what was causing the problem exactly.

Your solution of checking the fullscreen state every frame and resizing the buffers there instead of using WM_SIZE is also what I am using, except my rendering code is in a separate thread, and I also handle the Alt+ENTER manually because I didn't want to constrain my window size.

This topic is closed to new replies.

Advertisement