SDL_GetDisplayUsableBounds returns initial bounds
I'm currently on Ubuntu 24.04 LTS and have an application that is causing an assert due to an incorrect work area.
I am on 2.30.0 (I think this is the default on the OS?)
I tried to look through the logs / issues and did not see anything similar.
Issue: I am calling SDL_GetDisplayUsableBounds and it returns 0 (success), yet the values are all 0,0,0,0 (SDL_GetError also returns no message)
I have three displays, and the issue seems to occur when I have a reserved area (task bar) on the right- hand side of the preceding display. (left-hand sides are fine)
On my main / middle display I also have a menu bar at the top, but then the side panel as well. And moving the side panel from the left (where SDL_GetDisplayUsableBounds returns the correct bounds for all displays) to the right, seems to break the following display's metrics.
I wasn't able to replicate this exact issue, but I noticed a very similar issue with the usable bounds and the panel on X11.
These tests probably won't fix the issue, but can give some insights into how X11 display bounds work.
My issue was on X11, when having the panel on the top or bottom of any display, it affected all usable bounds on every display.
This code was used to print the display bounds:
Code
To compile with SDL2, predefine: -DMY_SDL_MAJOR_VERSION=2
To compile with SDL3, predefine: -DMY_SDL_MAJOR_VERSION=3
Optionally: On SDL3, to compile with Wayland instead with X11, predefine -DMY_USE_WAYLAND
#if MY_SDL_MAJOR_VERSION == 2
#include <SDL2/SDL.h>
#include <stdbool.h>
#elif MY_SDL_MAJOR_VERSION == 3
#include <SDL3/SDL.h>
#else
#error define MY_SDL_MAJOR_VERSION as 2 or 3
#endif
#include <stdio.h>
#include <string.h>
// print error message if value is false
//
// check if error message was set if value is true
//
// returns original value
static int my_print_SDL_error(bool val) {
const char *error_msg = SDL_GetError();
if (!val) {
printf("ERROR: %s\n", error_msg);
}
else if (error_msg[0]) {
printf("TRUE_BUT_ERROR_MESSAGE_SET: %s\n", error_msg);
}
SDL_ClearError();
return val;
}
// "main" function =================================================================================
int main(void) {
bool return_value;
#if MY_SDL_MAJOR_VERSION == 3 && defined(MY_USE_WAYLAND)
return_value = SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "wayland");
my_print_SDL_error(return_value);
#endif
// init SDL
#if MY_SDL_MAJOR_VERSION == 2
return_value = SDL_Init(SDL_INIT_VIDEO) == 0;
#elif MY_SDL_MAJOR_VERSION == 3
return_value = SDL_Init(SDL_INIT_VIDEO);
#endif
if (!my_print_SDL_error(return_value)) {
printf("ERROR: %s\n", SDL_GetError());
return -1;
}
// print SDL version
printf("SDL Version (linked): ");
#if MY_SDL_MAJOR_VERSION == 2
SDL_version linked_version;
SDL_GetVersion(&linked_version);
printf("%d.%d.%d\n", linked_version.major, linked_version.minor, linked_version.patch);
#elif MY_SDL_MAJOR_VERSION == 3
printf("%d.%d.%d\n", SDL_VERSIONNUM_MAJOR(SDL_GetVersion()),
SDL_VERSIONNUM_MINOR(SDL_GetVersion()),
SDL_VERSIONNUM_MICRO(SDL_GetVersion()));
#endif
// print video driver
const char * current_video_driver = SDL_GetCurrentVideoDriver();
printf("SDL_GetCurrentVideoDriver(): ");
printf("%s\n", current_video_driver ? current_video_driver : "NULL");
my_print_SDL_error(current_video_driver);
// num displays variable (and SDL3 display ID array)
#if MY_SDL_MAJOR_VERSION == 2
printf("SDL_GetNumVideoDisplays(): ");
const int num_video_displays = SDL_GetNumVideoDisplays();
printf("%d\n", num_video_displays);
#elif MY_SDL_MAJOR_VERSION == 3
printf("SDL_GetDisplays(): ");
int display_count;
SDL_DisplayID * displays = SDL_GetDisplays(&display_count);
printf("display_count = %d\n", display_count);
my_print_SDL_error(displays);
#endif
// print primary display
#if MY_SDL_MAJOR_VERSION == 3
printf("SDL_GetPrimaryDisplay(): ID = ");
const SDL_DisplayID primary_display_id = SDL_GetPrimaryDisplay();
printf("%d\n", primary_display_id);
my_print_SDL_error(primary_display_id);
#endif
// get max display name len
size_t max_display_name_len = 0;
#if MY_SDL_MAJOR_VERSION == 2
for (int display_idx = 0; display_idx < num_video_displays; ++display_idx) {
#elif MY_SDL_MAJOR_VERSION == 3
for (int display_idx = 0; display_idx < display_count; ++display_idx) {
#endif
#if MY_SDL_MAJOR_VERSION == 2
const char * const display_name = SDL_GetDisplayName(display_idx);
#elif MY_SDL_MAJOR_VERSION == 3
const char * const display_name = SDL_GetDisplayName(displays[display_idx]);
#endif
if (display_name) {
const size_t display_name_len = strlen(display_name);
if (display_name_len > max_display_name_len) {
max_display_name_len = display_name_len;
}
}
}
if (max_display_name_len < 4) {
max_display_name_len = 4;
}
// print display info table --------------------------------------------------------------------
printf("Display Info:\n");
// print header
printf("idx | ");
#if MY_SDL_MAJOR_VERSION == 3
printf("ID | ");
#endif
printf("%-*s | bounds | usable bounds\n", (int)max_display_name_len, "name");
printf("----|");
#if MY_SDL_MAJOR_VERSION == 3
printf("----|");
#endif
for (int num_dashes_to_print = (int)max_display_name_len + 2; num_dashes_to_print > 0; --num_dashes_to_print) {
printf("-");
}
printf("|-----------------------|----------------------\n");
// loop start
#if MY_SDL_MAJOR_VERSION == 2
for (int display_idx = 0; display_idx < num_video_displays; ++display_idx) {
#elif MY_SDL_MAJOR_VERSION == 3
for (int display_idx = 0; display_idx < display_count; ++display_idx) {
#endif
// print idx
printf("%d | ", display_idx);
// print ID
#if MY_SDL_MAJOR_VERSION == 3
printf("%d | ", displays[display_idx]);
#endif
// print name
#if MY_SDL_MAJOR_VERSION == 2
const char * const display_name = SDL_GetDisplayName(display_idx);
#elif MY_SDL_MAJOR_VERSION == 3
const char * const display_name = SDL_GetDisplayName(displays[display_idx]);
#endif
printf("%-*s | ", (int)max_display_name_len, display_name ? display_name : "NULL");
my_print_SDL_error(display_name);
SDL_Rect display_bounds = {0,0,0,0};
// print display bounds
#if MY_SDL_MAJOR_VERSION == 2
return_value = SDL_GetDisplayBounds(display_idx, &display_bounds) == 0;
#elif MY_SDL_MAJOR_VERSION == 3
return_value = SDL_GetDisplayBounds(displays[display_idx], &display_bounds);
#endif
printf("{%4d,%4d,%4d,%4d} | ", display_bounds.x, display_bounds.y, display_bounds.w, display_bounds.h);
my_print_SDL_error(return_value);
// print display usable bounds
#if MY_SDL_MAJOR_VERSION == 2
return_value = SDL_GetDisplayUsableBounds(display_idx, &display_bounds) == 0;
#elif MY_SDL_MAJOR_VERSION == 3
return_value = SDL_GetDisplayUsableBounds(displays[display_idx], &display_bounds);
#endif
printf("{%4d,%4d,%4d,%4d}", display_bounds.x, display_bounds.y, display_bounds.w, display_bounds.h);
my_print_SDL_error(return_value);
printf("\n");
} // loop end
// cleanup and quit
#if MY_SDL_MAJOR_VERSION == 3
SDL_free(displays);
#endif
SDL_Quit();
return 0;
}
My setup:
3 displays: 1920x1080, 2560x1440, 1920x1080 Ordered from left to right. they are all aligned at the top. KDE Plasma
Results:
I tested SDL2.30.0, SDL2.30.9 and the newest commit of the git master branch for SDL3. The results were the same across these three versions.
Result 1:
Panel display: middle Panel position: bottom Panel size: 100
SDL Version (linked): 3.1.7
SDL_GetCurrentVideoDriver(): x11
SDL_GetDisplays(): display_count = 3
SDL_GetPrimaryDisplay(): ID = 1
Display Info:
idx | ID | name | bounds | usable bounds
----|----|--------------|-----------------------|----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1340}
1 | 2 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920,1080}
2 | 3 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920,1080}
Result 1, is my normal setup(the panel height is bigger for demonstration purposes). All values look good. The panel size was subtracted from the middle display's height to get the usable bound's height.
For further results, only the display table will be shown, X11 is used, with a panel size of 100.
Result 2:
Panel display: left or middle or right Panel position: top
idx | ID | name | bounds | usable bounds
----|----|--------------|-----------------------|----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 100,2560,1340}
1 | 2 | DP-1 27" | {4480, 0,1920,1080} | {4480, 100,1920, 980}
2 | 3 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 100,1920, 980}
When the panel is at the top, the panel-height is added to display bound's y and subtracted from the height to get the usable bounds. It doesn't matter on which display the panel is. If the panel is at the top on either display, the usable bounds will be adjusted for all displays. This was a surprise, I thought it would only adjust the usable bounds of the display the panel was on, but it adjusts it for all displays.
Result 3:
Panel display: left or right Panel position: bottom
idx | ID | name | bounds | usable bounds
----|----|--------------|-----------------------|----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560, 980}
1 | 2 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920, 980}
2 | 3 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920, 980}
This result was even more surprising then the second one. The usable bound's height doesn't just get subtracted, it even crops the height of the middle display to 980. After the second result, I expected the height of the middle display to be subtracted to 1340, but it gets cropped to the same height as the left and right display.
Result 4:
Panel display: left Panel position: left
idx | ID | name | bounds | usable bounds
----|----|--------------|-----------------------|----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1440}
1 | 2 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920,1080}
2 | 3 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 100, 0,1820,1080}
Here, everything seems correct. The panel size was added to x and subtracted from the width to get the usable bounds of the left display.
Result 5:
Panel display: middle or right Panel position: left
idx | ID | name | bounds | usable bounds
----|----|--------------|-----------------------|----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1440}
1 | 2 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920,1080}
2 | 3 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920,1080}
Here, the panel was ignored and the usable bounds are the same as the display bounds.
Result 6:
Panel display: left or middle Panel side: right
idx | ID | name | bounds | usable bounds
----|----|--------------|-----------------------|----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1440}
1 | 2 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920,1080}
2 | 3 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920,1080}
The panel was ignored again.
Result 7:
Panel display: right Panel position: right
idx | ID | name | bounds | usable bounds
----|----|--------------|-----------------------|----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1440}
1 | 2 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1820,1080}
2 | 3 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920,1080}
This one seems correct again. It was subtracted from the right display's usable bound width.
Result Summary:
It seems like, if the panel is in between two displays, the panel is ignored and the usable bounds are the same as the display bounds. And if the panel is on the outside, it gets computed correctly. BUT, if any display crosses the line of the panel of another display, it gets cropped.
Were do these values come from?
SDL seems to get the usable bounds for a display from an X11 function: https://github.com/libsdl-org/SDL/blob/332fd824f04f3944bd59fff14fe2cc8f0bdfd866/src/video/x11/SDL_x11modes.c#L1017-L1027 I'm not sure if SDL has any influence on what X11 returns for the usable bounds.
And Wayland?
With Wayland the panel was always ignored and the returned usable bounds were always the same as the display bounds.
@Sackzement, can you print out the value of rect and usable in each of those case, before the intersection? I'm guessing the usable rect is relative to the monitor rect, or something like that.
@Kontrabant, do we expect Wayland to provide usable bounds here?
@Kontrabant, do we expect Wayland to provide usable bounds here?
No, unfortunately, there is no reliable way to query the usable desktop bounds. We do apply the toplevel window content size hints for resizable windows now though, so even if windows are slightly larger than the usable desktop space, they won't spill off of the edges.
@Sackzement, can you print out the value of rect and usable in each of those case, before the intersection? I'm guessing the usable rect is relative to the monitor rect, or something like that.
Here are the results with the rect and usable values right before the intersection:
Results 2 and 3 are the problematic ones, because the usable bounds of other displays get cropped as well. Especially in result 3 they get cropped by a lot.
This is an illustration of result 3:
yellow represents the panel
green are the resulting usable bounds
red are cropped regions
Panel size: 100
Result 1:
Panel display: middle Panel position: bottom
SDL Version (linked): 3.1.9
SDL_GetCurrentVideoDriver(): x11
SDL_GetDisplays(): display_count = 3
SDL_GetPrimaryDisplay(): ID = 1
Display Info:
idx | ID | name | bounds | usable bounds | rect before intersect | usable before intersect
----|----|--------------|-----------------------|-----------------------|-----------------------|-----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1340} | {1920, 0,2560,1440} | { 0, 0,6400,1340}
1 | 2 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920,1080} | { 0, 0,1920,1080} | { 0, 0,6400,1340}
2 | 3 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920,1080} | {4480, 0,1920,1080} | { 0, 0,6400,1340}
Result 2:
Panel display: left or middle or right Panel position: top
idx | ID | name | bounds | usable bounds | rect before intersect | usable before intersect
----|----|--------------|-----------------------|-----------------------|-----------------------|-----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 100,2560,1340} | {1920, 0,2560,1440} | { 0, 100,6400,1340}
1 | 2 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 100,1920, 980} | { 0, 0,1920,1080} | { 0, 100,6400,1340}
2 | 3 | DP-1 27" | {4480, 0,1920,1080} | {4480, 100,1920, 980} | {4480, 0,1920,1080} | { 0, 100,6400,1340}
Result 3:
Panel display: left or right Panel position: bottom
idx | ID | name | bounds | usable bounds | rect before intersect | usable before intersect
----|----|--------------|-----------------------|-----------------------|-----------------------|-----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560, 980} | {1920, 0,2560,1440} | { 0, 0,6400, 980}
1 | 2 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920, 980} | { 0, 0,1920,1080} | { 0, 0,6400, 980}
2 | 3 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920, 980} | {4480, 0,1920,1080} | { 0, 0,6400, 980}
Result 4:
Panel display: left Panel position: left
idx | ID | name | bounds | usable bounds | rect before intersect | usable before intersect
----|----|--------------|-----------------------|-----------------------|-----------------------|-----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1440} | {1920, 0,2560,1440} | { 100, 0,6300,1440}
1 | 2 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 100, 0,1820,1080} | { 0, 0,1920,1080} | { 100, 0,6300,1440}
2 | 3 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920,1080} | {4480, 0,1920,1080} | { 100, 0,6300,1440}
Result 5:
Panel display: middle or right Panel position: left
idx | ID | name | bounds | usable bounds | rect before intersect | usable before intersect
----|----|--------------|-----------------------|-----------------------|-----------------------|-----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1440} | {1920, 0,2560,1440} | { 0, 0,6400,1440}
1 | 2 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920,1080} | { 0, 0,1920,1080} | { 0, 0,6400,1440}
2 | 3 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920,1080} | {4480, 0,1920,1080} | { 0, 0,6400,1440}
Result 6:
Panel display: left or middle Panel side: right
idx | ID | name | bounds | usable bounds | rect before intersect | usable before intersect
----|----|--------------|-----------------------|-----------------------|-----------------------|-----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1440} | {1920, 0,2560,1440} | { 0, 0,6400,1440}
1 | 2 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920,1080} | { 0, 0,1920,1080} | { 0, 0,6400,1440}
2 | 3 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1920,1080} | {4480, 0,1920,1080} | { 0, 0,6400,1440}
Result 7:
Panel display: right Panel position: right
idx | ID | name | bounds | usable bounds | rect before intersect | usable before intersect
----|----|--------------|-----------------------|-----------------------|-----------------------|-----------------------
0 | 1 | DP-3 31" | {1920, 0,2560,1440} | {1920, 0,2560,1440} | {1920, 0,2560,1440} | { 0, 0,6300,1440}
1 | 2 | HDMI-A-1 27" | { 0, 0,1920,1080} | { 0, 0,1920,1080} | { 0, 0,1920,1080} | { 0, 0,6300,1440}
2 | 3 | DP-1 27" | {4480, 0,1920,1080} | {4480, 0,1820,1080} | {4480, 0,1920,1080} | { 0, 0,6300,1440}
So that all makes sense. What's happening is that the usable bounds aren't per-monitor, they're a rectangle across all three monitors that is guaranteed not to intersect the panel. Unfortunately, this means that depending on the panel position, we're left with a small rectangle across otherwise completely open monitors.
Here's someone asking about this on Stack Overflow: https://stackoverflow.com/questions/2598580/gtk-get-usable-area-of-each-monitor-excluding-panels
X-Tile solves this by taking the monitor bounds and subtracting the areas covered by strut windows: https://github.com/giuspen/x-tile/blob/9ec59c93f3991ab16975fee3215a6236833aa9a2/modules/support.py#L291-L315
I'm not sure if this approach is a good one for our purposes or not. @Sackzement, do you want to prototype it up and see how it works on your desktop?
In any case, changing how this works needs more testing time than we have for 3.2.0, so I'm bumping it out of the milestone.
I'm not sure if this approach is a good one for our purposes or not. @Sackzement, do you want to prototype it up and see how it works on your desktop?
This goes a bit beyond my very little knowledge of X11 and this issue isn't really a priority of mine. Just wanted to post some test results while briefly looking into this issue so the problem might be easier to pinpoint.
Thank you for the research, it's very helpful.
This is the layout (positions and sizes) of my 3 monitors that are side-by side in X11.
Monitor 0: DPI 1.08, SIZE 3840x2160 - POS 1920,0 - (Work 3656x2128 - 1920,32)
Monitor 1: DPI 1.51, SIZE 1920x1200 - POS 0,42
Monitor 2: DPI 1.70, SIZE 0x0 - POS 0,0
My layout is: 1 - 0 - 2 with 0 being my primary display with the taskbars on both top and right side (thus the work area)
The issue is then the zeroes on Monitor 2, and I found that removing the right-hand side taskbar OR moving it to the left side, fixes the issue.
More detailed environment versioning:
OS Platform: Linux Linux 6.8.0-51-generic #52-Ubuntu SMP PREEMPT_DYNAMIC Thu Dec 5 13:09:44 UTC 2024
SDL Ver.: 2.30.9
SDL Mixer Ver.: 2.8.0
OpenGL Ver.: OpenGL ES 3.2 NVIDIA 550.120
OpenGL Renderer: NVIDIA GeForce RTX 3050 Ti Laptop GPU/PCIe/SSE2
OpenGL Shader: OpenGL ES GLSL ES 3.20
Linux UI Env.: x11
Linux Compositor: <No window claimed _NET_WM_CM_S0 (no compositor?)>
Backend Renderer: imgui_impl_opengl3
Backend Platform: imgui_impl_sdl2
For now I just detect these values and return the screen size since I cannot rely on the error result.