libsdl-org / SDL

Simple Directmedia Layer
https://libsdl.org
zlib License
9.52k stars 1.77k forks source link

SDL2: SDL_SetWindowDisplayMode sometimes doesn't set the window size properly #8544

Open eddebaby opened 10 months ago

eddebaby commented 10 months ago

I think I have found a bug in SDL2 2.28.5. I have done some research, but I didn't reach a conclusion yet.

The bugs seems very similar to #3869 which I though might have been fixed by #4392...

However, I only get the bug in a far more narrow circumstance than described in #3869.

I believe the circumstances required to see the bug are:

This is what it looks like after I have switched to a 1080p mode from a 800x600 mode (with the above circumstances being true): image

Leaving the window and then returning to the window fixes it.

The workaround provided in #3869 works for me, see code sample below.

SDL_Window *window;
Uint32 sdlFlags;
int display;
int width;
int height;
char* title;

if (window != NULL)
{
    Uint32 current_fullscreen_flags = SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP;
    Uint32 new_fullscreen_flags = sdlFlags & SDL_WINDOW_FULLSCREEN_DESKTOP;
    if (new_fullscreen_flags == SDL_WINDOW_FULLSCREEN)
    {
        SDL_DisplayMode dm = { SDL_PIXELFORMAT_UNKNOWN, width, height, 0, 0};
        if (SDL_SetWindowDisplayMode(window, &dm) != 0)
        {
            return -1;
        }
        SDL_SetWindowSize(window, width, mdinfo->Height); // needed to fix bug
    }
    if (current_fullscreen_flags != new_fullscreen_flags)
    {
        if (SDL_SetWindowFullscreen(window, new_fullscreen_flags) != 0)
        {
            return -1;
        }
    }
    SDL_SetWindowBordered(window, (sdlFlags & SDL_WINDOW_BORDERLESS) ? SDL_FALSE : SDL_TRUE);
    if (new_fullscreen_flags == 0)
    {
        SDL_SetWindowSize(window, width, height);
        SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED_DISPLAY(display), SDL_WINDOWPOS_CENTERED_DISPLAY(display));
    }
}
else
{
    window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED_DISPLAY(display), SDL_WINDOWPOS_CENTERED_DISPLAY(display), width, height, sdlFlags);
}
icculus commented 10 months ago

What platform is this happening on? Windows? X11? Mac?

eddebaby commented 10 months ago

Windows 10.

Note: The code above is dummy code, let me know if you need a built/buildable example and I'll work on one.

If it will suffice, you can see the above snippet in practice here: https://github.com/dkfans/keeperfx/blob/2a1f91b7331d53e3a82df207f0583eb2091c89f8/src/bflib_video.c#L549

furious-programming commented 8 months ago

I have a similar problem with this feature. Very often, the resolution is not applied correctly, and this function never correctly sets the desired mode on a display other than the primary one. The case concerns SDL 2.28.5, on updated Windows 10.

The test program looks like below and has the minimum code needed to demonstrate the problem. If any function returns an error code, its content will be displayed in the console (along with the code line number). I don't have any error, so only logs appear in the console.

Changing display:

Changing window mode:

Press Esc to quit.

If the display or video mode with the requested index does not exist, nothing is changed. If a window was requested to be moved to a display other than the current one, the video mode is reset to index 0, i.e. the mode with the highest available resolution is used. After starting the program, you have 3 seconds to move the console window — in case you want to have it on a different screen and see the logs all the time.

Release (Win x64) — Exclusive.zip Source code:

{$mode objfpc}{$H+}

uses
  SDL2;
label
  Cleanup;
var
  Window:       PSDL_Window;
  WindowRect:   TSDL_Rect;
  Renderer:     PSDL_Renderer;
  Event:        TSDL_Event;
  DisplayMode:  TSDL_DisplayMode;
  DisplayIndex: Int32 = 0;
  ModeIndex:    Int32 = 0;
  InputIndex:   Int32;
begin
  SDL_Init  (SDL_INIT_VIDEO);
  SDL_Delay (3000);

  Window   := SDL_CreateWindow   ('Demo', SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 600, 450, 0);
  Renderer := SDL_CreateRenderer (Window, -1, SDL_RENDERER_ACCELERATED or SDL_RENDERER_PRESENTVSYNC);

  if SDL_GetDisplayMode       (DisplayIndex, ModeIndex, @DisplayMode) < 0 then WriteLn({$I %LINE%}, ' - ', SDL_GetError());
  if SDL_SetWindowDisplayMode (Window, @DisplayMode)                  < 0 then WriteLn({$I %LINE%}, ' - ', SDL_GetError());
  if SDL_SetWindowFullscreen  (Window, SDL_WINDOW_FULLSCREEN)         < 0 then WriteLn({$I %LINE%}, ' - ', SDL_GetError());

  SDL_GetWindowPosition       (Window, @WindowRect.X, @WindowRect.Y);
  SDL_GetWindowSize           (Window, @WindowRect.W, @WindowRect.H);

  WriteLn('Initial mode: ', DisplayMode.W, 'x', DisplayMode.H, ', ',   DisplayMode.Refresh_Rate, 'Hz');
  WriteLn('Actual:       ', WindowRect.W,  'x', WindowRect.H,  ' at ', WindowRect.X, ',', WindowRect.Y, ' (display: ', SDL_GetWindowDisplayIndex(Window), ')'#13#10);

  while True do
  begin
    while SDL_PollEvent(@Event) = 1 do
    case Event.Type_ of
      SDL_KEYDOWN:
      if Event.Key.Repeat_ = 0 then
      case Event.Key.KeySym.Scancode of
        SDL_SCANCODE_F1 .. SDL_SCANCODE_F12:
        begin
          InputIndex := Event.Key.KeySym.Scancode - SDL_SCANCODE_F1;

          if (InputIndex <> DisplayIndex) and (InputIndex < SDL_GetNumVideoDisplays()) then
          begin
            DisplayIndex := InputIndex;
            ModeIndex    := 0;

            if SDL_GetDisplayMode       (DisplayIndex, ModeIndex, @DisplayMode) < 0 then WriteLn({$I %LINE%}, ' - ', SDL_GetError());
            if SDL_SetWindowDisplayMode (Window, @DisplayMode)                  < 0 then WriteLn({$I %LINE%}, ' - ', SDL_GetError());

            SDL_GetWindowPosition       (Window, @WindowRect.X, @WindowRect.Y);
            SDL_GetWindowSize           (Window, @WindowRect.W, @WindowRect.H);

            WriteLn('New mode: ', DisplayMode.W, 'x', DisplayMode.H, ', ',   DisplayMode.Refresh_Rate, 'Hz (display: ', DisplayIndex, ')');
            WriteLn('Actual:   ', WindowRect.W,  'x', WindowRect.H,  ' at ', WindowRect.X, ',', WindowRect.Y, ' (display: ', SDL_GetWindowDisplayIndex(Window), ')'#13#10);
          end;
        end;

        SDL_SCANCODE_1 .. SDL_SCANCODE_9:
        begin
          InputIndex := Event.Key.KeySym.Scancode - SDL_SCANCODE_1;

          if (InputIndex <> ModeIndex) and (ModeIndex < SDL_GetNumDisplayModes(DisplayIndex)) then
          begin
            ModeIndex := InputIndex;

            if SDL_GetDisplayMode       (DisplayIndex, ModeIndex, @DisplayMode) < 0 then WriteLn({$I %LINE%}, ' - ', SDL_GetError());
            if SDL_SetWindowDisplayMode (Window, @DisplayMode)                  < 0 then WriteLn({$I %LINE%}, ' - ', SDL_GetError());

            SDL_GetWindowPosition       (Window, @WindowRect.X, @WindowRect.Y);
            SDL_GetWindowSize           (Window, @WindowRect.W, @WindowRect.H);

            WriteLn('New mode: ', DisplayMode.W, 'x', DisplayMode.H, ', ',   DisplayMode.Refresh_Rate, 'Hz (display: ', DisplayIndex, ')');
            WriteLn('Actual:   ', WindowRect.W,  'x', WindowRect.H,  ' at ', WindowRect.X, ',', WindowRect.Y, ' (display: ', SDL_GetWindowDisplayIndex(Window), ')'#13#10);
          end;
        end;

        SDL_SCANCODE_ESCAPE: goto Cleanup;
      end;

      SDL_QUITEV: goto Cleanup;
    end;

    WindowRect.X := 0;
    WindowRect.Y := 0;
    SDL_GetWindowSize(Window, @WindowRect.W, @WindowRect.H);

    SDL_SetRenderDrawColor (Renderer, 0, 0, 0, 255);
    SDL_RenderClear        (Renderer);
    SDL_SetRenderDrawColor (Renderer, 100, 100, 100, 255);
    SDL_RenderFillRect     (Renderer, @WindowRect);
    SDL_RenderPresent      (Renderer);
  end;

Cleanup:
  SDL_DestroyRenderer (Renderer);
  SDL_DestroyWindow   (Window);
  SDL_Quit            ();
end.

First problem — setting lower resolution on the primary display

When you run the program, it retrieves information about the highest resolution mode of the primary display, then turns on the exclusive video mode and sets the window to this mode. Everything works fine here and the result is as follows:

DSC_0025

The log appears in the console:

Initial mode: 1280x800, 60Hz
Actual:       1280x800 at 0,0 (display: 0)

Now I press the 9 key to change the resolution to a smaller one (still to primary display). On my laptop, this mode has a resolution of 640x400. Unfortunately, it doesn't work properly, the result is as follows:

DSC_0028

In the console, I see:

New mode: 640x400, 60Hz (display: 0)
Actual:   640x400 at 0,0 (display: 0)

The window size is changed to 640x400, but the screen resolution is not adjusted to the window size and is still the same as before, as seen by the mouse pointer size (i.e. 1280x800). Interestingly, SDL changes the window size and screen resolution, but for some reason it immediately changes the resolution to the previous one, leaving the target window size. During the change, you can see that the resolution changes (the mouse cursor becomes large for a moment), see the video:

https://github.com/libsdl-org/SDL/assets/8170730/da32824c-2e43-41cf-849a-b8f20988257c

If I now press the 1 key, the resolution will be correctly set to native. So setting a lower resolution doesn't work properly, because SDL apparently changes the window size but can't set the appropriate resolution (or accidentally reverts it back to the previous one).


Second problem — running exclusive video mode on the secondary display

The window has a native size of 1280x800 and is displayed on the primary screen. I press the F2 key and the program is supposed to get information about the first available video mode of the second display (with the highest resolution and refresh rate) and show the window on the second display using this mode. The highest resolution mode on my monitor is 1280x1024 (75Hz).

This is where the problems arise. SDL does not move the window and does not change its resolution at all. Instead, it tries to set this mode on the primary display, but since it does not support 1280x1024 resolution (and 75Hz), nothing happens. The following log appears in the console:

New mode: 1280x1024, 75Hz (display: 1)
Actual:   1280x800 at 0,0 (display: 0)

As you can see in the log, the window stays in place. For anything to change, I must select a monitor mode that has a resolution no greater than the resolution of the laptop screen. So I press the 9 key, the monitor mode with parameters 1024x768 75Hz is selected and this mode is activated not on the monitor, but on the laptop:

DSC_0030

The log appears in the console:

New mode: 1024x768, 75Hz (display: 1)
Actual:   1024x768 at 0,0 (display: 0)

The program knows that the window is on the monitor, but it is displayed on the laptop screen. The line with the Actual log gives the position and size read by the SDL functions on the fly, so SDL sees that the window is on the laptop screen, inconsistent with the set mode.

Now if I press the F1 key, the program will read the first available display mode of the laptop, which is 1280x800, and set it for the window. And while the window size is set correctly (as seen by the program and SDL), the laptop screen resolution still remains the same as before, i.e. 1024x768. The situation only changes when I minimize the window, e.g. by clicking on something on the second screen, and then restore it from the taskbar. Only then will the screen resolution be set to the correct one.


Conclusion

Setting the window mode using SDL_SetWindowDisplayMode almost never works properly. Changing the resolution of the window displayed on the primary display is done and the window changes size, but the screen resolution either remains unchanged (and the window is smaller than the screen) or is set to incorrect.

Trying to set a designated video mode on a secondary display never works properly — SDL always displays the window on the primary display anyway (or does not change anything if you choose a higher resolution than the highest resolution of the primary display). The position of the secondary display does not matter - by default I have the monitor on the left of the laptop (coordinates -1280,0), moving the monitor to the right of the laptop (coordinates 1280,0) does not change anything.

My display layout looks like this:

image

furious-programming commented 8 months ago

I'm just checking whether the SDL_SetWindowDisplayMode function worked well on previous versions of SDL2. I have downloaded and tested literally every single release version published on GitHub (all that had .dlls compiled for Windows 64-bit).

In short, changing the window mode worked correctly in SDL 2.0.18, however, only if the window was displayed on the primary display. This stopped working properly in SDL 2.24.0 and remained so until the current version.

Displaying a window in an exclusive video mode on a non-primary screen worked just as badly as it does now (same symptoms). I haven't found any version of SDL2 where this works properly (at least on my laptop and setup).

I hope this information will help you find bugs and fix them.

Kontrabant commented 8 months ago

Renderer := SDL_CreateRenderer (Window, -1, SDL_RENDERER_ACCELERATED or SDL_RENDERER_PRESENTVSYNC);

By default, SDL2 creates a direct3d 9 render backend, and d3d9, for some reason, is quite broken when switching to exclusive fullscreen modes on the non-primary display, moving fullscreen windows, etc… If you use create a renderer with a newer d3d version or opengl (try the envvar SDL_RENDER_DRIVER=direct3d12), does it behave as expected?

SDL3 moved the d3d9 renderer down in priority due to issues like this (and since XP is basically dead at this point).

furious-programming commented 8 months ago

If you use create a renderer with a newer d3d version or opengl (try the envvar SDL_RENDER_DRIVER=direct3d12), does it behave as expected?

Setting the hint SDL_HINT_RENDER_DRIVER to direct3d/direct3d12 does not change anything at all — changing the resolution still does not work correctly on either the primary or secondary display. However, setting this hint to opengl allows to correctly change the resolution on the primary display, but there is still exactly the same problem with the secondary display.

So even if I use the OpenGL backend, secondary display support still doesn't work at all.

furious-programming commented 8 months ago

I found another problem with fullscreen mode. If I set the hint SDL_HINT_RENDER_DRIVER to opengl, it is impossible to set desktop fullscreen mode — SDL runs exclusive fullscreen anyway. On the other hand, if I do not set the opengl driver, desktop fullscreen can be set, but then changing the window/display resolution on the main screen will no longer work (as I showed earlier).

Fullscreen support is seriously f*****d up. :(

furious-programming commented 8 months ago

Ok, I finally dealt with it and implemented exclusive fullscreen support in my engine, for any display and any display mode it supports. Unfortunately, I couldn't get the Direct3D backend to do this, but if I set OpenGL it works quite well. In case anyone is interested in this issue or if someone came here from a search engine, I wrote below how I did it.


The backend should be set to OpenGL by setting the SDL_RENDER_DRIVER hint to opengl.

Fullscreen dekstop can be set directly using the SDL_SetWindowFullscreen function and specifying the SDL_WINDOW_FULLSCREEN_DESKTOP flag. You don't need to do anything extra and it should work on any display.

Fullscreen exclusive, regardless of which display it is to be launched on and with what display mode, should be launched in as follows:

The order in which the above functions are called is crucial. While exclusive fullscreen should work properly on any display and in any supported display mode, each time you change the display mode, it will redundantly disable fullscreen and restore it at the end, which causes additional screen flashing when changing modes (it takes two or three seconds at most).


OpenGL set as the backend does not support desktop fullscreen in the normal way. On the main screen, it runs a semi-exclusive fullscreen anyway, while on the secondary display this mode works as standard.

The second thing is that displaying the window in exclusive fullscreen on any screen and with a lower resolution than the native one causes a strange problem. When the mode setting is completed, simply move the mouse cursor to another screen (outside the window area), and when the cursor returns to the window area, SDL spontaneously sets the window size to native (and queues window resize events) and a large part of it does not fit in screen. This happens every time, for any display and any display mode. To avoid this, it is necessary to completely turn off fullscreen and turn it on again each time you change the display mode (I described it in the points above).

slouken commented 8 months ago

Thank you for the detailed information. I don't want to introduce any regressions, so I'm moving this out of the 2.30 milestone.

SDL 3.0 has completely revamped fullscreen support, are you seeing the same issues there?

furious-programming commented 8 months ago

I don't want to introduce any regressions, so I'm moving this out of the 2.30 milestone.

Ok, that's understandable. Overall, I'm happy with what I have now, because although there are some inconveniences (a little more screen flashing when changing display modes), ultimately the exclusive fullscreen works very well for the OpenGL backend.

SDL 3.0 has completely revamped fullscreen support, are you seeing the same issues there?

I haven't tested SDL 3.0 yet because I develop my engine in Free Pascal and I don't touch C at all. To be able to test anything, I need a compiled x64 .dll, and I also need to (re)write my headers for Free Pascal. I'm currently using headers that work well with SDL2 and I wouldn't want to mislead anyone in case my headers and not SDL are to blame.

I will switch to SDL3 only when its first stable version is published. I will then rewrite my headers, make sure they are correct, and start testing in conjunction with my engine. And I will definitely switch to SDL3 because of the new functionality that SDL2 does not have, so it's only a matter of time.


If you want to be sure that fullscreen support works properly on various displays, backends and platforms, I suggest to prepare a tester and simply check whether everything is fine. If necessary, I will be happy to carry out tests — just send me a compiled test program (e.g. similar to the one shown in this post). I am also on SDL's Discord, if anyone need to discuss and exchange files (testers, screenshots, recordings, etc.).

slouken commented 8 months ago

Great, thanks!

martinstarkov commented 15 hours ago

When switching from fullscreen to windowed mode in SDL2 on Windows 10 (150% display scaling), I noticed that occasionally the window size is not set to its pre-fullscreen size. So for example, if my window is 800x800, and I set it to fullscreen and then back to windowed mode, it would sometimes return to 812x834 with a black bar below the window border and to the right of the drawing area. Image to illustrate the effect: image

Relevant code that led to this behavior:

const auto set_windowed = [](SDL_Window* window) {
    int flags = SDL_GetWindowFlags(window);
    if ((flags & SDL_WINDOW_FULLSCREEN) == 0) {
        return;
    }
    SDL_SetWindowFullscreen(window, 0);
};

const auto set_fullscreen = [](SDL_Window* window) {
    int flags = SDL_GetWindowFlags(window);
    if ((flags & SDL_WINDOW_FULLSCREEN) == SDL_WINDOW_FULLSCREEN) {
        return;
    }
    SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
};

if (/* key press check */) {
   set_windowed(window);
}
if (/* key press check */) {
   set_fullscreen(window);
}

For anyone arriving here due to searching for a fix for the above issue, I played around with @furious-programming's list of steps mentioned above and noticed that adding

    SDL_DisplayMode mode;
    SDL_GetCurrentDisplayMode(0, &mode);
    SDL_SetWindowDisplayMode(window, &mode);

prior to setting the window to fullscreen fixes the black bar issue. The fixed code to switch between windowed mode and exclusive fullscreen is then:

const auto set_windowed = [](SDL_Window* window) {
    int flags = SDL_GetWindowFlags(window);
    if ((flags & SDL_WINDOW_FULLSCREEN) == 0) {
        return;
    }
    SDL_SetWindowFullscreen(window, 0);
};

const auto set_fullscreen = [](SDL_Window* window) {
    int flags = SDL_GetWindowFlags(window);
    if ((flags & SDL_WINDOW_FULLSCREEN) == SDL_WINDOW_FULLSCREEN) {
        return;
    }
    SDL_DisplayMode mode;
    SDL_GetCurrentDisplayMode(0, &mode);
    SDL_SetWindowDisplayMode(window, &mode);
    SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
};

Usage as before:

if (/* key press check */) {
   set_windowed(window);
}
if (/* key press check */) {
   set_fullscreen(window);
}