Skip to content

Unblock the main loop while resizing on Windows - #3752

Open
eXpl0it3r wants to merge 2 commits into
masterfrom
bugfix/win32-render-while-resizing
Open

eXpl0it3r wants to merge 2 commits into
masterfrom
bugfix/win32-render-while-resizing

Conversation

@eXpl0it3r

Copy link
Copy Markdown
Member

Description

This is another proposal in fixing the long standing "unable to render while resizing or dragging the window" issue on Windows. See also the solution design for it: https://github.com/SFML/SFML/wiki/SD:-Render-while-Resizing

Other proposals:

Quick recap on the problem: While resizing, clicking the title bar / moving the window, the event dispatching goes into Windows own event loop (i.e. the modal loop) and doesn't return control to the caller, which means SFML applications pause on pollEvents().

This PR essentially uses the "threading method", but instead of threads it uses Fibers. Fibers are separate call stacks on the same thread, which allows SFML to pause the modal loop and return control to the main loop and its own fiber.

The second commit fixes the issue, where clicking on the title bar also blocks on the modal loop and right clicking blocks until the mouse button is released.

This PR fixes #3016

See also:

Tasks

  • Tested on Windows

How to test this PR?

Resize the window, drag it, right click on the title bar, click & hold on the title bar.

#include <SFML/Graphics.hpp>

#include <algorithm>
#include <string>

int main()
{
    sf::RenderWindow window(sf::VideoMode({640, 480}), "Resize me");
    window.setVerticalSyncEnabled(true);

    sf::RectangleShape square({100.f, 100.f});
    square.setOrigin({50.f, 50.f});
    square.setFillColor(sf::Color(200, 120, 40));

    sf::RectangleShape outline;
    outline.setFillColor(sf::Color::Transparent);
    outline.setOutlineColor(sf::Color::Green);
    outline.setOutlineThickness(-4.f);

    const sf::Clock time;
    sf::Clock       frameClock;
    sf::Clock       titleClock;
    sf::Time        worstGap;
    int             frames = 0;

    while (window.isOpen())
    {
        while (const std::optional event = window.pollEvent())
        {
            if (event->is<sf::Event::Closed>())
                window.close();
            else if (const auto* resized = event->getIf<sf::Event::Resized>())
                window.setView(sf::View(sf::FloatRect({0.f, 0.f}, sf::Vector2f(resized->size))));
        }

        worstGap = std::max(worstGap, frameClock.restart());
        ++frames;
        if (titleClock.getElapsedTime() >= sf::seconds(1))
        {
            window.setTitle("Resize me - " + std::to_string(frames) + " FPS, worst frame gap " +
                            std::to_string(worstGap.asMilliseconds()) + " ms");
            frames   = 0;
            worstGap = sf::Time::Zero;
            titleClock.restart();
        }

        const sf::Vector2f size(window.getSize());
        square.setPosition(size / 2.f);
        square.setRotation(sf::degrees(time.getElapsedTime().asSeconds() * 90.f));
        outline.setSize(size);

        window.clear(sf::Color(30, 30, 60));
        window.draw(outline);
        window.draw(square);
        window.display();
    }
}

This PR

librewolf_XgKa3dVJic

1601 PR

librewolf_WYdQfkBAnc

3390 PR

librewolf_bAOwFnNMBh

JonnyPtn's commit

librewolf_7YRuTERFey

@eXpl0it3r
eXpl0it3r force-pushed the bugfix/win32-render-while-resizing branch 4 times, most recently from a4f856b to 7dabdba Compare September 12, 2026 09:11
Using Fibers (instead of threads) the otherwise blocking modal loop can
be paused and control can be returned to the main loop fiber.
@eXpl0it3r
eXpl0it3r force-pushed the bugfix/win32-render-while-resizing branch from 7dabdba to cfdb9d6 Compare September 12, 2026 09:16
@eXpl0it3r

eXpl0it3r commented Sep 12, 2026 •

Copy link
Copy Markdown
Member Author

And just because it's fun:

explorer_P65zfdUWGb
⚠️ LLM Code
////////////////////////////////////////////////////////////
// Five windows, one cube.
//
// The cube lives at a position on the *virtual desktop*, not inside any
// particular window. Every window is just a hole you look through: whatever
// part of the cube happens to be behind that hole gets drawn. Move or resize a
// window and the cube slides into view; move the cube with the arrow keys and
// it wanders from window to window.
////////////////////////////////////////////////////////////

#include <SFML/Graphics.hpp>

#include <windows.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <vector>

namespace
{
////////////////////////////////////////////////////////////
// Minimal 3D scaffolding, just enough for one cube
////////////////////////////////////////////////////////////
struct Vec3
{
    float x{};
    float y{};
    float z{};
};

Vec3 operator-(Vec3 lhs, Vec3 rhs)
{
    return {lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z};
}

Vec3 cross(Vec3 lhs, Vec3 rhs)
{
    return {lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x};
}

float dot(Vec3 lhs, Vec3 rhs)
{
    return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
}

Vec3 normalize(Vec3 v)
{
    const float length = std::sqrt(dot(v, v));
    return length > 0.f ? Vec3{v.x / length, v.y / length, v.z / length} : v;
}

Vec3 rotate(Vec3 p, float angleX, float angleY, float angleZ)
{
    const float sinX = std::sin(angleX);
    const float cosX = std::cos(angleX);
    const float sinY = std::sin(angleY);
    const float cosY = std::cos(angleY);
    const float sinZ = std::sin(angleZ);
    const float cosZ = std::cos(angleZ);

    Vec3 r{p.x, p.y * cosX - p.z * sinX, p.y * sinX + p.z * cosX};
    r = Vec3{r.x * cosY + r.z * sinY, r.y, -r.x * sinY + r.z * cosY};
    return {r.x * cosZ - r.y * sinZ, r.x * sinZ + r.y * cosZ, r.z};
}

////////////////////////////////////////////////////////////
// The cube: eight corners, six quads
////////////////////////////////////////////////////////////
constexpr std::array<Vec3, 8> cubeCorners{
    {{-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, {-1, 1, -1}, {-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}}};

struct Face
{
    std::array<std::size_t, 4> corners{};
    sf::Color                  color;
};

const std::array<Face, 6> cubeFaces{{{{0, 1, 2, 3}, sf::Color(220, 70, 90)},
                                     {{5, 4, 7, 6}, sf::Color(80, 170, 230)},
                                     {{4, 0, 3, 7}, sf::Color(250, 190, 60)},
                                     {{1, 5, 6, 2}, sf::Color(110, 210, 140)},
                                     {{4, 5, 1, 0}, sf::Color(190, 120, 230)},
                                     {{3, 2, 6, 7}, sf::Color(240, 140, 70)}}};

constexpr float cubeScale      = 95.f;  //!< Pixels per unit at the projection plane
constexpr float cameraDistance = 4.5f;  //!< How far the camera sits from the cube center
constexpr float focalLength    = 3.f;   //!< Perspective strength
constexpr float cubeMoveSpeed  = 420.f; //!< Pixels per second
constexpr float gridSpacing    = 80.f;  //!< Spacing of the desktop grid, in pixels

sf::Color shade(sf::Color color, float factor)
{
    const auto apply = [factor](std::uint8_t channel)
    { return static_cast<std::uint8_t>(std::clamp(static_cast<float>(channel) * factor, 0.f, 255.f)); };
    return {apply(color.r), apply(color.g), apply(color.b)};
}

////////////////////////////////////////////////////////////
// Everything we need to know about one of our windows
////////////////////////////////////////////////////////////
struct Hole
{
    std::unique_ptr<sf::RenderWindow> window;
    sf::Color                         accent;
};

////////////////////////////////////////////////////////////
// Where the client area of a window starts on the virtual desktop
//
// sf::WindowBase::getPosition() returns the position of the window frame, so
// using it directly would offset the cube by the title bar height. Asking Win32
// for the client origin keeps the illusion pixel perfect.
////////////////////////////////////////////////////////////
sf::Vector2f clientOrigin(const sf::WindowBase& window)
{
    POINT point{0, 0};
    ClientToScreen(window.getNativeHandle(), &point);
    return {static_cast<float>(point.x), static_cast<float>(point.y)};
}
} // namespace


////////////////////////////////////////////////////////////
int main()
{
    const auto desktop = sf::Vector2f(sf::VideoMode::getDesktopMode().size);

    // Five windows: three on the upper row, two staggered below
    const sf::Vector2u windowSize{460, 360};
    const auto         size    = sf::Vector2f(windowSize);
    const float        originX = std::max(20.f, (desktop.x - (3.f * size.x + 2.f * 40.f)) / 2.f);
    const float        originY = std::max(40.f, (desktop.y - (2.f * size.y + 40.f)) / 2.f);

    const std::array<sf::Vector2f, 5> layout{{{originX, originY},
                                              {originX + size.x + 40.f, originY},
                                              {originX + 2.f * (size.x + 40.f), originY},
                                              {originX + 0.5f * (size.x + 40.f), originY + size.y + 40.f},
                                              {originX + 1.5f * (size.x + 40.f), originY + size.y + 40.f}}};

    const std::array<sf::Color, 5> accents{{sf::Color(230, 90, 110),
                                            sf::Color(90, 180, 235),
                                            sf::Color(245, 190, 70),
                                            sf::Color(120, 215, 150),
                                            sf::Color(195, 125, 235)}};

    std::vector<Hole> holes;
    for (std::size_t i = 0; i < layout.size(); ++i)
    {
        auto window = std::make_unique<sf::RenderWindow>(sf::VideoMode(windowSize),
                                                         "Window " + std::to_string(i + 1) + " - peek at the cube");
        window->setPosition(sf::Vector2i(layout[i]));
        window->setVerticalSyncEnabled(false);
        holes.push_back({std::move(window), accents[i]});
    }

    std::cout << "Five windows, one cube.\n\n"
              << "  Arrow keys   move the cube across the desktop\n"
              << "  Shift        move faster\n"
              << "  Space        recenter the cube\n"
              << "  Escape       quit\n\n"
              << "Drag or resize any window and watch the cube stay put in desktop space.\n"
              << std::flush;

    // The cube is at home on the desktop, not in a window
    sf::Vector2f cubePosition = desktop / 2.f;

    sf::Clock clock;
    sf::Clock frameClock;
    bool      quit = false;

    while (!quit && !holes.empty())
    {
        const float dt = std::min(frameClock.restart().asSeconds(), 0.1f);

        bool focused = false;
        for (auto& hole : holes)
        {
            while (const std::optional event = hole.window->pollEvent())
            {
                if (event->is<sf::Event::Closed>())
                {
                    hole.window->close();
                }
                else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>())
                {
                    if (keyPressed->code == sf::Keyboard::Key::Escape)
                        quit = true;
                    else if (keyPressed->code == sf::Keyboard::Key::Space)
                        cubePosition = desktop / 2.f;
                }
            }

            if (hole.window->isOpen() && hole.window->hasFocus())
                focused = true;
        }

        // Only steer the cube while one of our windows is in the foreground
        if (focused)
        {
            const bool  fast = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::LShift) ||
                              sf::Keyboard::isKeyPressed(sf::Keyboard::Key::RShift);
            const float speed = cubeMoveSpeed * (fast ? 3.f : 1.f);

            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left))
                cubePosition.x -= speed * dt;
            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right))
                cubePosition.x += speed * dt;
            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Up))
                cubePosition.y -= speed * dt;
            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Down))
                cubePosition.y += speed * dt;
        }

        // Project the cube once per frame, in desktop coordinates
        const float elapsed = clock.getElapsedTime().asSeconds();

        std::array<sf::Vector2f, 8> projected{};
        std::array<Vec3, 8>         rotated{};
        for (std::size_t i = 0; i < cubeCorners.size(); ++i)
        {
            rotated[i] = rotate(cubeCorners[i], elapsed * 0.7f, elapsed * 1.1f, elapsed * 0.35f);

            const float depth  = rotated[i].z + cameraDistance;
            const float factor = focalLength / depth * cubeScale;
            projected[i]       = cubePosition + sf::Vector2f{rotated[i].x * factor, -rotated[i].y * factor};
        }

        // Painter algorithm: farthest face first
        std::array<std::size_t, 6> order{0, 1, 2, 3, 4, 5};
        std::sort(order.begin(),
                  order.end(),
                  [&](std::size_t lhs, std::size_t rhs)
                  {
                      const auto depth = [&](std::size_t face)
                      {
                          float sum = 0.f;
                          for (const std::size_t corner : cubeFaces[face].corners)
                              sum += rotated[corner].z;
                          return sum;
                      };
                      return depth(lhs) > depth(rhs);
                  });

        const Vec3 light = normalize({0.4f, 0.6f, -0.7f});

        sf::VertexArray triangles(sf::PrimitiveType::Triangles);
        sf::VertexArray outline(sf::PrimitiveType::Lines);
        for (const std::size_t index : order)
        {
            const Face& face   = cubeFaces[index];
            const Vec3  normal = normalize(cross(rotated[face.corners[1]] - rotated[face.corners[0]],
                                                rotated[face.corners[3]] - rotated[face.corners[0]]));
            const sf::Color color = shade(face.color, 0.45f + 0.55f * std::max(0.f, dot(normal, light)));

            for (const std::size_t corner : {0u, 1u, 2u, 0u, 2u, 3u})
                triangles.append(sf::Vertex{projected[face.corners[corner]], color});

            for (std::size_t corner = 0; corner < 4; ++corner)
            {
                outline.append(sf::Vertex{projected[face.corners[corner]], sf::Color(20, 20, 28)});
                outline.append(sf::Vertex{projected[face.corners[(corner + 1) % 4]], sf::Color(20, 20, 28)});
            }
        }

        // Bounding box of the cube, used to light up the windows it peeks through
        sf::Vector2f cubeMin = projected[0];
        sf::Vector2f cubeMax = projected[0];
        for (const sf::Vector2f point : projected)
        {
            cubeMin = {std::min(cubeMin.x, point.x), std::min(cubeMin.y, point.y)};
            cubeMax = {std::max(cubeMax.x, point.x), std::max(cubeMax.y, point.y)};
        }

        for (auto& hole : holes)
        {
            auto& window = *hole.window;
            if (!window.isOpen())
                continue;

            const auto viewSize = sf::Vector2f(window.getSize());
            if (viewSize.x == 0.f || viewSize.y == 0.f)
                continue;

            // Refresh the view every frame so the cube keeps its desktop position
            // even while the window is being resized
            window.setView(sf::View(sf::FloatRect({0.f, 0.f}, viewSize)));

            // The window is a hole in the desktop, so shift everything by its origin
            const sf::Vector2f origin = clientOrigin(window);
            sf::Transform      toWindow;
            toWindow.translate(-origin);

            const bool showsCube = cubeMax.x > origin.x && cubeMin.x < origin.x + viewSize.x &&
                                   cubeMax.y > origin.y && cubeMin.y < origin.y + viewSize.y;

            window.clear(sf::Color(24, 26, 34));

            // A grid anchored to the desktop, so all windows share one space
            sf::VertexArray grid(sf::PrimitiveType::Lines);
            const auto      line = [&](sf::Vector2f from, sf::Vector2f to, sf::Color color)
            {
                grid.append(sf::Vertex{from, color});
                grid.append(sf::Vertex{to, color});
            };

            const float firstX = std::floor(origin.x / gridSpacing) * gridSpacing;
            for (float x = firstX; x < origin.x + viewSize.x; x += gridSpacing)
            {
                const bool major = std::fmod(std::abs(x), gridSpacing * 5.f) < 0.5f;
                line({x, origin.y}, {x, origin.y + viewSize.y}, major ? sf::Color(62, 66, 84) : sf::Color(38, 41, 52));
            }

            const float firstY = std::floor(origin.y / gridSpacing) * gridSpacing;
            for (float y = firstY; y < origin.y + viewSize.y; y += gridSpacing)
            {
                const bool major = std::fmod(std::abs(y), gridSpacing * 5.f) < 0.5f;
                line({origin.x, y}, {origin.x + viewSize.x, y}, major ? sf::Color(62, 66, 84) : sf::Color(38, 41, 52));
            }

            window.draw(grid, toWindow);
            window.draw(triangles, toWindow);
            window.draw(outline, toWindow);

            // Accent frame, brighter while the cube is in sight
            sf::RectangleShape frame({viewSize.x - 4.f, viewSize.y - 4.f});
            frame.setPosition({2.f, 2.f});
            frame.setFillColor(sf::Color::Transparent);
            frame.setOutlineThickness(-2.f);
            frame.setOutlineColor(showsCube ? hole.accent : shade(hole.accent, 0.35f));
            window.draw(frame);

            window.display();
        }

        holes.erase(std::remove_if(holes.begin(), holes.end(), [](const Hole& hole) { return !hole.window->isOpen(); }),
                    holes.end());

        // Rough 60 Hz cap, the windows are not vsynced so they do not stack up
        const sf::Time spent = frameClock.getElapsedTime();
        if (spent < sf::milliseconds(16))
            sf::sleep(sf::milliseconds(16) - spent);
    }
}

@eXpl0it3r

Copy link
Copy Markdown
Member Author

Any of the contributors / commenters on previous discussions and PRs, can you take a look at / test this PR, @hobby8, @1aam2am1, @eliasdaler, @learn-more, @trustytrojan, @JonnyPtn, @MarioLiebisch, @MirrasHue, @antcode123?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to update the window while resizing or dragging the window

1 participant