Skip to content

Conversation

@Jipok
Copy link
Contributor

@Jipok Jipok commented Jan 5, 2026

Description

When running SDL3 applications on tiling window managers like i3, moving a window to an invisible workspace does not trigger SDL_WINDOW_MINIMIZED or SDL_WINDOW_HIDDEN. Consequently, the application continues rendering at full speed (VSync dependent), consuming unnecessary GPU/CPU resources even when not visible.

When a workspace is hidden, i3(and possible other tiling WMs) unmaps the container and sets the client window state to WithdrawnState (via the WM_STATE atom). Previously, the SDL3 X11 backend ignored changes to WM_STATE during PropertyNotify events, failing to detect this transition.

I provide a test program to log window flags and FPS:

Details
// gcc sdl3_test.c -o sdl3_test -lSDL3
// Code written by LLM
#include <SDL3/SDL.h>
#include <stdio.h>

void LogFlags(SDL_Window *window) {
    Uint32 flags = SDL_GetWindowFlags(window);
    printf("[STATUS] Flags: ");
    if (flags & SDL_WINDOW_FULLSCREEN) printf("FULLSCREEN ");
    if (flags & SDL_WINDOW_OCCLUDED) printf("OCCLUDED (Перекрыто) ");
    if (flags & SDL_WINDOW_HIDDEN) printf("HIDDEN (Скрыто) ");
    if (flags & SDL_WINDOW_MINIMIZED) printf("MINIMIZED ");
    if (flags & SDL_WINDOW_MAXIMIZED) printf("MAXIMIZED ");
    if (flags & SDL_WINDOW_INPUT_FOCUS) printf("INPUT_FOCUS ");
    if (flags & SDL_WINDOW_MOUSE_FOCUS) printf("MOUSE_FOCUS ");
    printf("\n");
}

int main(int argc, char *argv[]) {
    if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
        fprintf(stderr, "SDL init error: %s\n", SDL_GetError());
        return 1;
    }

    SDL_Window *window = SDL_CreateWindow("SDL3 i3 Test", 800, 600, SDL_WINDOW_RESIZABLE);
    if (!window) return 1;

    int running = 1;
    Uint64 last_tick = SDL_GetTicks();
    int frames = 0;

    LogFlags(window);

    while (running) {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
                case SDL_EVENT_QUIT: running = 0; break;
                
                case SDL_EVENT_WINDOW_SHOWN:     printf("[EVENT] WINDOW_SHOWN\n"); break;
                case SDL_EVENT_WINDOW_HIDDEN:    printf("[EVENT] WINDOW_HIDDEN\n"); break;
                case SDL_EVENT_WINDOW_EXPOSED:   printf("[EVENT] WINDOW_EXPOSED\n"); break;
                case SDL_EVENT_WINDOW_MINIMIZED: printf("[EVENT] WINDOW_MINIMIZED (!!!)\n"); break;
                case SDL_EVENT_WINDOW_RESTORED:  printf("[EVENT] WINDOW_RESTORED\n"); break;
                case SDL_EVENT_WINDOW_OCCLUDED:  printf("[EVENT] WINDOW_OCCLUDED\n"); break;
                
                case SDL_EVENT_WINDOW_FOCUS_GAINED: printf("[EVENT] FOCUS_GAINED\n"); break;
                case SDL_EVENT_WINDOW_FOCUS_LOST:   printf("[EVENT] FOCUS_LOST\n"); break;

                // Noise
                case SDL_EVENT_MOUSE_MOTION: 
                case SDL_EVENT_WINDOW_MOVED:
                    break;

                default:
                    // printf("[EVENT] Unknown: 0x%x\n", event.type);
                    break;
            }
        }

        // Work emulation
        frames++;
        Uint8 r = (frames % 255);
        SDL_Surface *surface = SDL_GetWindowSurface(window);
        const SDL_PixelFormatDetails *details = SDL_GetPixelFormatDetails(surface->format);
        Uint32 rectColor = SDL_MapRGB(details, NULL, r, 0, 0);
        SDL_FillSurfaceRect(surface, NULL, rectColor);
        SDL_UpdateWindowSurface(window);

        // Ping every second
        Uint64 current_tick = SDL_GetTicks();
        if (current_tick - last_tick >= 1000) {
            printf("--- PING: FPS: %d ---\n", frames);
            LogFlags(window); 
            frames = 0;
            last_tick = current_tick;
        }
    }

    SDL_Quit();
    return 0;
}

Before fix:

[STATUS] Flags: INPUT_FOCUS
[EVENT] WINDOW_EXPOSED
[EVENT] WINDOW_SHOWN
[EVENT] FOCUS_GAINED
--- PING: FPS: 586 ---
[STATUS] Flags: INPUT_FOCUS
--- PING: FPS: 582 ---
[STATUS] Flags: INPUT_FOCUS
--- PING: FPS: 604 ---                  
[STATUS] Flags: INPUT_FOCUS
[EVENT] FOCUS_LOST
--- PING: FPS: 758 ---                <-- Low because visible
[STATUS] Flags: MAXIMIZED
--- PING: FPS: 1640 ---               <-- Spikes because invisible, but app thinks it's active
[STATUS] Flags: MAXIMIZED
--- PING: FPS: 1632 ---
[STATUS] Flags: MAXIMIZED
--- PING: FPS: 1623 ---

After fix:

[STATUS] Flags: INPUT_FOCUS
[EVENT] FOCUS_LOST
[EVENT] WINDOW_MINIMIZED (!!!)
--- PING: FPS: 1306 ---
[STATUS] Flags: MINIMIZED

Existing Issue(s)

I'm sure I've seen somewhere that people are complaining about games on i3wm hogging the CPU/GPU in the background, but I can't find it now.

@slouken slouken added this to the 3.4.2 milestone Jan 5, 2026
@slouken slouken requested a review from Kontrabant January 5, 2026 23:05
@Kontrabant
Copy link
Contributor

Can you squash the commits into one for merging?

@slouken slouken merged commit 7931321 into libsdl-org:main Jan 7, 2026
44 of 45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants