Window content does not exactly fit window size
Hello all,
I initially opened an issue for SDL2. As the problems obviously are still here with SDL3 I think it is time to open an updated issue.
- My application preserves aspect ratio of its window when resizing it. After resizing there are almost always black lines without content at the borders of the window (bottom or right border). It seems the window is slightly larger than needed after resize and does not exactly fit rendered content. These function calls are used to configure the window and renderer:
SDL_Window* sdlWindow = SDL_CreateWindow(PROG_NAME, width, height, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
SDL_SetWindowAspectRatio(sdlWindow, (float)width/height, (float)width/height);
SDL_Renderer* sdlRenderer = SDL_CreateRenderer(sdlWindow, NULL);
SDL_SetRenderLogicalPresentation(sdlRenderer, width, height, SDL_LOGICAL_PRESENTATION_LETTERBOX, SDL_SCALEMODE_LINEAR);
The variables width and height are integers.
- My application has a bar at the bottom of the window that can be shown or hidden. When changing visibility of the bar, the height of the window is changed appropriately. When hiding and then showing the bar again, the size of the window often shrinks by one logical pixel. It sometimes also leads to issue 1 (black lines at borders). This code is used:
SDL_GetWindowSize(sdlWindow, &w, NULL);
SDL_SetWindowAspectRatio(sdlWindow, (float)width/height, (float)width/height);
SDL_SetWindowSize(sdlWindow, w, (height*w)/width);
SDL_SetRenderLogicalPresentation(sdlRenderer, width, height, SDL_LOGICAL_PRESENTATION_LETTERBOX, SDL_SCALEMODE_LINEAR);
The variable w is an integer. The value of height is changed when adjusting visibility of the bar.
Screenshots:
Window after start-up with no black lines:

Window after resize with black line at bottom:

Window after resize with black line on right border:

Can you nail it down to a fault with a specific SDL function? Are you sure it's not a float / int logic issue in your app? Can you make a minimal example to reproduce this behaviour?
I think all relevant code is in the above description.
In the first issue the only math done is calculating the aspect ratio with (float)width/height. The initial rendering is correct. So I think that one works. The problem occurs when I resize the window by pulling the edges of the window. My application is not involved in this process.
In the second issue there is some additional math when setting the new window size. The height is calculated by height*w/width. That one only uses integers. So there are no conversions involved.
I think if you use the code of issue one and fill the window with white color you should be able to reproduce the first issue by resizing the window. If you add the code of issue two and change height back and forth the second issue should be visible.
In my application width is 1120 and height is 856. Height can be switched to 832 and back to 856 (second issue).
If you let the window width or height vary freely and just try to adjust the one that did not change then there is often no way to preserve the aspect ratio exactly.
For example, if the aspect ratio is 16:9 and the size 1920×1080 then you would have to change the width in steps of 16 and the height in steps of 9. The closest sizes that preserve the aspect ratio exactly would be 1904×1071 and 1936×1089. Everything else in-between would be slightly wrong.
In your program there is actually two places where SDL tries to adjust for the aspect ratio.
1. The window size (SDL_SetWindowAspectRatio)
The preservation of the window aspect ratio when the user resize the window is handled by the backend. On X11 it uses some X11 "hints". On other platforms it seems to often use rounding. Windows apparently just cast to int when the min and max aspect ratios are the same. In other words, the exact behavior could differ between platforms.
When I test on Linux with X11 there doesn't seem to be a one-to-one mapping between width and height. I can end up with different width for the same height and vice versa.
2. The letterboxing (SDL_SetRenderLogicalPresentation)
When "letterboxing" is used, SDL preserves either the width or height (whichever makes the whole area fit inside the window) and then it just scales and floors the other. The way this is done means that there will always be at least one pixel row or column left over when the window aspect ratio is not exactly what you specified (In an attempt to put things in the center it might actually render everything at half pixel offsets)
I'm not sure SDL is doing anything wrong. To make the window always the correct aspect ratio SDL would have to resize in "steps" instead of allowing any width/height but that wouldn't work for all aspect ratios and float rounding errors is also a concern.
When using "letterboxing" it is expected to get black borders when the window aspect ratio doesn't match the logical resolution which is exactly what happens here. If you don't want black border maybe you should use SDL_LOGICAL_PRESENTATION_STRETCH or SDL_LOGICAL_PRESENTATION_OVERSCAN instead?
Another option that I have used in the past is to not try to restrict the window aspect ratio at all and just set it to the correct aspect ratio to begin with and then let the user resize the window freely and use "letterboxing" to preserve the aspect ratio of the graphics. You can't force correct window aspect ratio all of the time anyway (e.g. when the window is maximized or in full screen).
At the moment I use a workaround. I set the renderer to SDL_LOGICAL_PRESENTATION_DISABLED and catch the resize event. Then I use this code to fit the window to the content.
float scale;
int h;
SDL_GetWindowSize(sdlWindow, NULL, &h);
scale = (float)h / height;
SDL_SetWindowSize(sdlWindow, width*scale, h);
SDL_SetRenderScale(sdlRenderer, scale*dpiFactor, scale*dpiFactor);
This is a bit too complicated for my taste (not what the S in SDL stands for) and causes problems with fullscreen mode and requires to find out high DPI stuff. I think it should scale the window the same way it scales the renderer. Then all should be fine.
Yeah, it's probably just a math issue in SDL_render.c when calculating the output area. Can you provide a simple example that we can use to repro and verify the fix?
I think this is enough to reproduce it. You can barely see the thin black line on Windows 10, but they are there, as the OP describes.
#include <SDL3/SDL_main.h>
#include <SDL3/SDL.h>
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
int width = 640, height = 480;
SDL_Window* window = SDL_CreateWindow(nullptr, width, height, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
SDL_SetWindowAspectRatio(window, (float)width / height, (float)width / height);
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
SDL_SetRenderLogicalPresentation(renderer, width, height, SDL_LOGICAL_PRESENTATION_LETTERBOX, SDL_SCALEMODE_LINEAR);
bool running = true;
while(running) {
SDL_PumpEvents();
SDL_Event event;
while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST) == 1) {
switch (event.type) {
case SDL_EVENT_QUIT:
running = false;
break;
}
}
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
I am waiting for this bug to be fixed since years. Please do not move this to later milestones.
Here's an updated test application that lets you see the black bars during the resize operation:
#include <SDL3/SDL.h>
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL_main.h>
static SDL_Window *window;
static SDL_Renderer *renderer;
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
SDL_Init(SDL_INIT_VIDEO);
int width = 640, height = 480;
window = SDL_CreateWindow("test aspect ratio", width, height, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
SDL_SetWindowAspectRatio(window, (float)width / height, (float)width / height);
renderer = SDL_CreateRenderer(window, NULL);
SDL_SetRenderLogicalPresentation(renderer, width, height, SDL_LOGICAL_PRESENTATION_LETTERBOX);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 0xFF, 0, 0, 0xFF);
SDL_RenderRect(renderer, NULL);
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
}
@Kaktus514 is correct, when resizing a window you can't maintain the exact pixel ratio unless you do stepwise resizing, which isn't a user experience that people would expect.
SDL is doing what you asked when passing SDL_LOGICAL_PRESENTATION_LETTERBOX, it's letterboxing the content with black bars in the case where the window size doesn't exactly match your desired aspect ratio. It sounds like you want to use SDL_LOGICAL_PRESENTATION_STRETCH, which will automatically do the scaling you're currently doing manually.
In any case, this is doing the best it can, given the constraints you've provided.
No, Kaktus514 is not correct. This is a bug. Obviously SDL uses different scaling methods for windows and renderers. This causes offsets that should not exist. If renderer and window was scaled the same way, there would be no issue.
I am using SDL for an emulator. Losing pixels or distorting window content therefore is not an option.
SDL_LOGICAL_PRESENTATION_STRETCH shows the correct content at all window sizes, and is roughly equivalent to your current workaround. You can use that mode in the example program above to see this in action.
If you have an idea of how to better handle a window width of 1919, I'd love to hear it. :)
Just use the same method of scaling for windows and renderers and all will be fine.
Let's take a very simple example.
Let's say your content is a 2x1 block of pixels and you've constrained your aspect ratio at 2. Let's say your window is 2x1. This is perfect! No scaling needed, the user sees pixels exactly the way you want. Now let's say the user resizes the window to 4x2. This is also perfect! The scale is 2.0 and the user sees pixels scaled up without losing any information. Now let's say the user widens the window just a little bit, to 5 pixels. We can't have a window that's 2.5 pixels high, so we round the window size to 5x3. What would you expect to happen to the content in this case?
Same size as the window.
Same size as the window.
And that's exactly what happens if you use SDL_LOGICAL_PRESENTATION_STRETCH.
But why does the letterboxing scale it to something else? It does not preserve exact aspect ratio either. There is some unpredictable scaling offset.
In our example above, the renderer would see a 5x3 window and know that it needs letterboxing. Since the output viewport must be whole pixels, it will round as close as possible. In this case it would use the existing width, 5, and round down for the height, to 2. You'd get a content area of 5x2 positioned at 0,0.5.
The renderer doesn't know that you want to constrain the aspect ratio of the content, it just knows that it should take the existing window size and letterbox it appropriately. Since it's not possible in this case to maintain the exact aspect ratio of the window, you no longer have the exact aspect ratio for the content.
If you wanted to maintain the aspect ratio for the content in a 5x3 window, you'd have to use 4x2 (since 6x3 wouldn't fit) and you'd have a 1 pixel letterbox on one side and either top or bottom. This isn't what you want either, so it really sounds like SDL_LOGICAL_PRESENTATION_STRETCH is what you want.
Why doesn‘t it round the same way as the window?
Because they're doing different jobs. If you want it to round the same way as the window, use SDL_LOGICAL_PRESENTATION_STRETCH.
It seems the problem got even worse with the latest revision of SDL3. It seems this patch is the cause: https://github.com/libsdl-org/SDL/commit/4add7e2005f745ef42e386cb75a34b006578d586
Switching to SDL_LOGICAL_PRESENTATION_STRETCH causes distortions while running in full screen. My applications has a status bar at the bottom of the window, which can be shown or hidden. Switching is done like this:
/* Get new heigt for our window */
height = NeXT_SCRN_HEIGHT + Statusbar_SetHeight(NeXT_SCRN_WIDTH, NeXT_SCRN_HEIGHT);
SDL_GetWindowSize(sdlWindow, &w, NULL);
SDL_SetWindowAspectRatio(sdlWindow, (float)width/height, (float)width/height);
SDL_SetWindowSize(sdlWindow, w, (height*w)/width);
SDL_SetRenderLogicalPresentation(sdlRenderer, width, height, SDL_LOGICAL_PRESENTATION_STRETCH);
StatusbarSetHeight() returns 0 if the statusbar is hidden. If I switch the status bar on or off in full screen, it will distort the content of the screen.
Can you provide specific numbers for all those variables? I'm going to drop them into testsprite and see if I can reproduce what you're seeing.
Also, you probably want to check the actual window size after you set it and flush the window operations with SDL_SyncWindow(), and then use SDL_LOGICAL_PRESENTATION_STRETCH if it's close to your desired aspect ratio, and SDL_LOGICAL_PRESENTATION_LETTERBOX if not. Setting a window size is a request and can be denied by window managers, and I believe fullscreen windows override both setting the size and aspect ratio.
static const int NeXT_SCRN_WIDTH = 1120;
static const int NeXT_SCRN_HEIGHT = 832;
int width = NeXT_SCRN_WIDTH;
int height = NeXT_SCRN_HEIGHT;
height += Statusbar_GetHeight();
Statusbar_GetHeight() and Statusbar_SetHeight() both return either 0 or 24.
Thank you for your efforts!
You're welcome!
FYI, I noticed that with some window sizes, toggling the status bar repeatedly shrinks the window. Here's the fix:
SDL_SetWindowSize(sdlWindow, w, (int)SDL_roundf((float)(height * w) / width));
Here's the patch to testsprite that I'm using to test this. I'm running into rendering issues, documented in https://github.com/libsdl-org/SDL/issues/11704, so we'll have to fix those before coming back to this.
diff --git a/test/testsprite.c b/test/testsprite.c
index fa04bfcfe..935e6c01c 100644
--- a/test/testsprite.c
+++ b/test/testsprite.c
@@ -39,6 +39,30 @@ static const int fps_check_delay = 5000;
static int use_rendergeometry = 0;
static bool suspend_when_occluded;
+static const int NeXT_SCRN_WIDTH = 1120;
+static const int NeXT_SCRN_HEIGHT = 832;
+
+static bool statusbar_enabled = true;
+
+static int Statusbar_GetHeight()
+{
+ return statusbar_enabled ? 24 : 0;
+}
+
+static void ToggleStatusBar(SDL_Window *sdlWindow, SDL_Renderer *sdlRenderer)
+{
+ statusbar_enabled = !statusbar_enabled;
+
+ int width = NeXT_SCRN_WIDTH;
+ int height = NeXT_SCRN_HEIGHT + Statusbar_GetHeight();
+ int w;
+
+ SDL_GetWindowSize(sdlWindow, &w, NULL);
+ SDL_SetWindowAspectRatio(sdlWindow, (float)width / height, (float)width / height);
+ SDL_SetWindowSize(sdlWindow, w, (int)SDL_roundf((float)(height * w) / width));
+ SDL_SetRenderLogicalPresentation(sdlRenderer, width, height, SDL_LOGICAL_PRESENTATION_STRETCH);
+}
+
/* Number of iterations to move sprites - used for visual tests. */
/* -1: infinite random moves (default); >=0: enables N deterministic moves */
static int iterations = -1;
@@ -87,6 +111,7 @@ static void MoveSprites(SDL_Renderer *renderer, SDL_Texture *sprite)
/* Query the sizes */
SDL_SetRenderViewport(renderer, NULL);
SDL_GetRenderSafeArea(renderer, &viewport);
+ viewport.h -= Statusbar_GetHeight();
SDL_SetRenderViewport(renderer, &viewport);
/* Cycle the color and alpha, if desired */
@@ -385,6 +410,14 @@ static void MoveSprites(SDL_Renderer *renderer, SDL_Texture *sprite)
SDL_free(indices2);
}
+ if (statusbar_enabled) {
+ viewport.y += viewport.h;
+ viewport.h = Statusbar_GetHeight();
+ SDL_SetRenderViewport(renderer, &viewport);
+ SDL_SetRenderDrawColor(renderer, 0xFF, 0x00, 0x00, 0xFF);
+ SDL_RenderFillRect(renderer, NULL);
+ }
+
/* Update the screen! */
SDL_RenderPresent(renderer);
}
@@ -546,6 +579,8 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
}
}
+ ToggleStatusBar(state->windows[0], state->renderers[0]);
+
/* Main render loop in SDL_AppIterate will begin when this function returns. */
frames = 0;
next_fps_check = SDL_GetTicks() + fps_check_delay;
@@ -558,6 +593,11 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_RENDER_DEVICE_RESET) {
LoadSprite(icon);
+ } else if (event->type == SDL_EVENT_KEY_DOWN) {
+ if (event->key.key == SDLK_SPACE) {
+ ToggleStatusBar(state->windows[0], state->renderers[0]);
+ }
+ return SDL_APP_CONTINUE;
}
return SDLTest_CommonEventMainCallbacks(state, event);
}
You're welcome!
FYI, I noticed that with some window sizes, toggling the status bar repeatedly shrinks the window. Here's the fix:
SDL_SetWindowSize(sdlWindow, w, (int)SDL_roundf((float)(height * w) / width));
Thank you! This improves things, but the window still occasionally shrinks by 1 or 2 pixels.
You're welcome! FYI, I noticed that with some window sizes, toggling the status bar repeatedly shrinks the window. Here's the fix:
SDL_SetWindowSize(sdlWindow, w, (int)SDL_roundf((float)(height * w) / width));Thank you! This improves things, but the window still occasionally shrinks by 1 or 2 pixels.
Yes, you'll never get exact floating point precision because you're working in integer pixels when resizing the window. As long as it stays stable once resized, it should be fine.
I went ahead and implemented the letterbox vs stretch approach that I suggested above, and the latest SDL code works great with this patch to testsprite:
diff --git a/test/testsprite.c b/test/testsprite.c
index e8d8a7dcb..bb3599fc5 100644
--- a/test/testsprite.c
+++ b/test/testsprite.c
@@ -39,6 +39,40 @@ static const int fps_check_delay = 5000;
static int use_rendergeometry = 0;
static bool suspend_when_occluded;
+static const int NeXT_SCRN_WIDTH = 1120;
+static const int NeXT_SCRN_HEIGHT = 832;
+
+static bool statusbar_enabled = true;
+
+static int Statusbar_GetHeight()
+{
+ return statusbar_enabled ? 24 : 0;
+}
+
+static void ToggleStatusBar(SDL_Window *sdlWindow, SDL_Renderer *sdlRenderer)
+{
+ statusbar_enabled = !statusbar_enabled;
+
+ int width = NeXT_SCRN_WIDTH;
+ int height = NeXT_SCRN_HEIGHT + Statusbar_GetHeight();
+ int w = 1, h = 1;
+
+ float desired_aspect = (float)width / height;
+ SDL_GetWindowSize(sdlWindow, &w, NULL);
+ SDL_SetWindowAspectRatio(sdlWindow, desired_aspect, desired_aspect);
+ SDL_SetWindowSize(sdlWindow, w, (int)SDL_roundf((float)(height * w) / width));
+ SDL_SyncWindow(sdlWindow);
+
+ float actual_aspect;
+ SDL_GetWindowSize(sdlWindow, &w, &h);
+ actual_aspect = (float)w / h;
+ if (fabsf(desired_aspect - actual_aspect) < 0.001f) {
+ SDL_SetRenderLogicalPresentation(sdlRenderer, width, height, SDL_LOGICAL_PRESENTATION_STRETCH);
+ } else {
+ SDL_SetRenderLogicalPresentation(sdlRenderer, width, height, SDL_LOGICAL_PRESENTATION_LETTERBOX);
+ }
+}
+
/* Number of iterations to move sprites - used for visual tests. */
/* -1: infinite random moves (default); >=0: enables N deterministic moves */
static int iterations = -1;
@@ -87,6 +121,7 @@ static void MoveSprites(SDL_Renderer *renderer, SDL_Texture *sprite)
/* Query the sizes */
SDL_SetRenderViewport(renderer, NULL);
SDL_GetRenderSafeArea(renderer, &viewport);
+ viewport.h -= Statusbar_GetHeight();
SDL_SetRenderViewport(renderer, &viewport);
/* Cycle the color and alpha, if desired */
@@ -385,6 +420,14 @@ static void MoveSprites(SDL_Renderer *renderer, SDL_Texture *sprite)
SDL_free(indices2);
}
+ if (statusbar_enabled) {
+ viewport.y += viewport.h;
+ viewport.h = Statusbar_GetHeight();
+ SDL_SetRenderViewport(renderer, &viewport);
+ SDL_SetRenderDrawColor(renderer, 0xFF, 0x00, 0x00, 0xFF);
+ SDL_RenderFillRect(renderer, NULL);
+ }
+
/* Update the screen! */
SDL_RenderPresent(renderer);
}
@@ -546,6 +589,8 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
}
}
+ ToggleStatusBar(state->windows[0], state->renderers[0]);
+
/* Main render loop in SDL_AppIterate will begin when this function returns. */
frames = 0;
next_fps_check = SDL_GetTicks() + fps_check_delay;
@@ -558,6 +603,11 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_RENDER_DEVICE_RESET) {
LoadSprite(icon);
+ } else if (event->type == SDL_EVENT_KEY_DOWN) {
+ if (event->key.key == SDLK_SPACE) {
+ ToggleStatusBar(state->windows[0], state->renderers[0]);
+ return SDL_APP_CONTINUE;
+ }
}
return SDLTest_CommonEventMainCallbacks(state, event);
}
I'm going to go ahead and close this issue now. Please open a new one if you run into more issues.