Search code examples
c++winapidirect3d11dxgi

Very low framerate when using full-screen window while multiple other windows are open


Sorry for the long title, but I have a very specific problem that can't really be expressed any more concisely. I'm programming a game engine (GitHub link: here) and I'm trying to let the client create windows on top of the main window which the application supplies automatically.

I've completely managed to get this to work but I'm bothered with the framerate of the main window when it enters full-screen mode (either on initialization or when the user presses alt+enter). I haven't benchmarked the performance, but it is visibly bad (so probably around 20-30 FPS) and the performance only drops when the user creates another window (it doesn't even have to be showing).

Since all of the windows the user creates are children of the main window, I have to hide them before entering full-screen mode.

I have a lot of code in my window class (over 1000 lines), so giving you a minimal example will be very difficult. If you must see the code, please visit the GitHub repo (under platform/windows you will find the code I'm referencing). I wonder if this is a strange artifact of having multiple windows open in the same process, or if I'm just missing some code.

That being said, here is some actual client code:

SandboxApp.h

#pragma once

#include<Infinity.h>

class SandboxApp : public Infinity::Application
{
private:
    Infinity::Window *m_popup_window;
    Infinity::Rasterizer *m_rasterizer;

    Infinity::OrthoCamera m_camera;
    Infinity::Renderer2D m_renderer;
    Infinity::Texture2D *m_texture;

    float m_aspect_ratio;

public:
    SandboxApp();
    ~SandboxApp();

    void OnApplicationEntered(Infinity::ApplicationEnteredEvent *event) override;

    void OnUserCreate(Infinity::UserCreateEvent *event) override;
    void OnUserUpdate(Infinity::UserUpdateEvent *event) override;
    void OnUserRender(Infinity::UserRenderEvent *event) override;
    void OnUserDestroy(Infinity::UserDestroyEvent *event) override;

    void OnWindowResized(Infinity::WindowResizedEvent *event) override;

    void Exit(const char *message);
};

SanboxApp.cpp

#define INFINITY_ENTRY_POINT
#include"SandboxApp.h"

SandboxApp::SandboxApp():
    m_popup_window(nullptr),
    m_rasterizer(nullptr),

    m_renderer(),
    m_texture(),

    m_aspect_ratio(),
    m_camera()
{}

SandboxApp::~SandboxApp()
{}

void SandboxApp::Exit(const char *message)
{
    INFINITY_CLIENT_ERROR(message);
    RequestExit();
}

void SandboxApp::OnApplicationEntered(Infinity::ApplicationEnteredEvent *event)
{
    Infinity::Window::WindowParams &params = event->GetMainWindowParams();
    params.fullscreen = true;
    params.auto_show = false;
}

void SandboxApp::OnUserCreate(Infinity::UserCreateEvent *event)
{
    Infinity::Window *window = GetMainWindow();
    m_popup_window = Infinity::Window::CreateWindow();

    Infinity::Window::WindowParams window_params;
    window_params.width = 300;
    window_params.height = 300;
    window_params.title = "Popup window!";

    if (!m_popup_window->Init(window_params))
    {
        Exit("Error initializing popup window");
        return;
    }

    // Set clear color
    Infinity::Context *context = Infinity::Window::GetContext();
    context->SetClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    m_popup_window->MakeContextCurrent();

    context = Infinity::Window::GetContext();
    context->SetClearColor(0.0f, 0.0f, 1.0f, 1.0f);

    window->MakeContextCurrent();

    // Initialize other resources
    m_rasterizer = Infinity::Rasterizer::CreateRasterizer();

    if (!m_rasterizer->Init(Infinity::Rasterizer::CullMode::NONE, true))
    {
        Exit("Error initializing rasterizer");
        return;
    }

    m_rasterizer->Bind();

    if (!m_renderer.Init())
    {
        Exit("Error initializing Renderer2D");
        return;
    }

    m_texture = Infinity::Texture2D::CreateTexture();

    if (!m_texture->Init("assets/image.png"))
    {
        Exit("Error initializing texture");
        return;
    }

    INFINITY_CLIENT_INFO("Client created");

    window->Show();
    m_popup_window->Show();

    event->Consume();
}

void SandboxApp::OnUserUpdate(Infinity::UserUpdateEvent *event)
{
    Infinity::Window *window = GetMainWindow();

    if (KeyPressed(Infinity::KeyCode::Escape))
    {
        if (window->CursorEnabled())
        {
            window->DisableCursor();
        }
        else
        {
            window->EnableCursor();
        }
    }

    if (window->CursorEnabled())
    {
        event->Consume();
        return;
    }

    float speed = (float)(3.0 * event->GetDT());
    float r_speed = (float)(2.0 * event->GetDT());
    float z_speed = (float)(1.0 * event->GetDT());

    if (KeyDown(Infinity::KeyCode::Left)) { m_camera.MoveLeft(speed); }
    if (KeyDown(Infinity::KeyCode::Right)) { m_camera.MoveRight(speed); }

    if (KeyDown(Infinity::KeyCode::Down)) { m_camera.MoveBackward(speed); }
    if (KeyDown(Infinity::KeyCode::Up)) { m_camera.MoveForward(speed); }

    if (KeyDown(Infinity::KeyCode::W)) { m_camera.zoom += z_speed; }
    if (KeyDown(Infinity::KeyCode::S)) { m_camera.zoom -= z_speed; }
    if (KeyDown(Infinity::KeyCode::A)) { m_camera.roll -= r_speed; }
    if (KeyDown(Infinity::KeyCode::D)) { m_camera.roll += r_speed; }

    m_camera.Update(m_aspect_ratio);

    event->Consume();
}

void SandboxApp::OnUserRender(Infinity::UserRenderEvent *event)
{
    Infinity::Window *window = GetMainWindow();
    window->MakeContextCurrent();

    Infinity::Context *context = Infinity::Window::GetContext();
    context->Clear();

    m_renderer.StartScene(&m_camera);

    Infinity::Renderer2D::QuadParams quad;
    quad.position = { 0.0f, 0.0f };
    quad.size = { 1.0f, 1.0f };
    quad.color = { 1.0f, 0.0f, 0.0f, 1.0f };

    m_renderer.DrawQuad(quad);

    m_renderer.EndScene();

    m_popup_window->MakeContextCurrent();

    context = Infinity::Window::GetContext();
    context->Clear();

    window->MakeContextCurrent();

    event->Consume();
}

void SandboxApp::OnUserDestroy(Infinity::UserDestroyEvent *event)
{
    m_renderer.Destroy();

    if (m_rasterizer)
    {
        m_rasterizer->Destroy();
        delete m_rasterizer;
    }

    if (m_texture)
    {
        m_texture->Destroy();
        delete m_texture;
    }

    if (m_popup_window)
    {
        m_popup_window->Destroy();
        delete m_popup_window;
    }

    INFINITY_CLIENT_INFO("Client destroyed");

    event->Consume();
}

void SandboxApp::OnWindowResized(Infinity::WindowResizedEvent *event)
{
    if (event->GetWindow() == GetMainWindow())
    {
        m_aspect_ratio = (float)event->GetWidth() / (float)event->GetHeight();

        m_camera.Update(m_aspect_ratio);
        event->Consume();
    }
}

Infinity::Application *Infinity::CreateApplication()
{
    return new SandboxApp;
}

If you need any other information, please just leave a comment.
Thanks in advance! :)

Update
I tried adding my executables to the Graphics Performance options list but it didn't change the low framerate of the full-screen window.

I did some more testing and found out that that I only need to create the sub-window for these inefficiencies to occur. Even if I don't show, update or render to the window, simply creating it slows down the frame rate of my full-screen main window.

Trying to do more research, I realized that MSDN does not have any documentation on using multiple DXGI swap chains. My hunch is that setting the full-screen state of one swap chain to true somehow interferes with the other swap chain causing inefficiencies (Although my ID3D11Device debug output doesn't mention inefficiencies anywhere)


Solution

  • There’re 2 kinds of full-screen in Windows.

    True fullscreen is probably what you’re doing, calling IDXGISwapChain::SetFullscreenState. In this mode, the windows desktop compositor, dwm.exe, is disabled, giving your app exclusive access to the monitor. However, as you found out it has complications. There’re more of them BTW: alt+tab can be slow, popups from other applications may glitch, and also it’s tricky to implement correctly, I think you have to re-create a swap chain and all render targets. On the bright side, in some rare cases it may improve FPS by a single-digit number.

    Borderless windowed is what you should be doing since you want multiple Win32 windows running along with your fullscreen content. In this mode the desktop compositor is up and running, updating your window along others. Switching in and out that mode is also way easier, example below.

    HRESULT WindowImpl::maximizeBorderless( bool max )
    {
        // https://devblogs.microsoft.com/oldnewthing/20100412-00/?p=14353
        DWORD dwStyle = GetWindowLong( GWL_STYLE );
    
        if( max )
        {
            MONITORINFO mi = { sizeof( mi ) };
            if( GetWindowPlacement( &m_wpPrev ) && GetMonitorInfo( MonitorFromWindow( m_hWnd, MONITOR_DEFAULTTOPRIMARY ), &mi ) )
            {
                SetWindowLong( GWL_STYLE, dwStyle & ~WS_OVERLAPPEDWINDOW );
                SetWindowPos( HWND_TOP,
                    mi.rcMonitor.left, mi.rcMonitor.top,
                    mi.rcMonitor.right - mi.rcMonitor.left,
                    mi.rcMonitor.bottom - mi.rcMonitor.top,
                    SWP_NOOWNERZORDER | SWP_FRAMECHANGED );
            }
        }
        else
        {
            SetWindowLong( GWL_STYLE, dwStyle | WS_OVERLAPPEDWINDOW );
            SetWindowPlacement( &m_wpPrev );
            SetWindowPos( NULL, 0, 0, 0, 0,
                SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
                SWP_NOOWNERZORDER | SWP_FRAMECHANGED );
        }
    
        return S_OK;
    }