libsdl-org / SDL

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

Window content does not exactly fit window size #10403

Open andreasgrabher opened 3 months ago

andreasgrabher commented 3 months ago

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.

  1. 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.

  1. 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:

normal

Window after resize with black line at bottom:

black line bottom

Window after resize with black line on right border:

black line right
AntTheAlchemist commented 3 months ago

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?

andreasgrabher commented 3 months ago

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).

Kaktus514 commented 3 months ago

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).

andreasgrabher commented 3 months ago

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.

slouken commented 3 months ago

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?

AntTheAlchemist commented 3 months ago

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;
}