Keep aspect ratio when resizing?

There are a few ancient posts about this, so I was wondering if there are any new tactics or developments that address this issue.

The goal is to keep the window aspect ratio as it is being resized.
As far as I understand, the current method is constantly setting the window size as you are resizing.

If there are no optimal solutions, maybe it would be best for me to use padding.
However, I’m uncertain what a good way would be:

  • black
  • solid color based on average pixel color of the last pixel row of the vertical/horizontal side
  • solid color based on median color of the last pixel row of the vertical/horizontal side
  • solid color based on median color of the whole image, and same on all sides
  • just copy the last row as needed (lines, looks bad)
  • stretch the whole renderer (looks bad)
  • … other

Please advise :slight_smile:

Not exactly, it can maintain aspect ratio or not. It depends on your goal.

IMO the window size should be free, the players should be able to resize the window as they want to and the game itself should adapt the game frame aspect ratio to the selected window size. If the aspect ratio of the window and game frame does not match, empty space should be filled with black. Some games fills the empty space with special (textured) pattern.

This is how it works in my engine — the window size can be any and the game frame is rendered centered in the window, maintaining the correct (fixed) aspect ratio. Back buffer has a size of 358x224 pixels.


Other solution is to fill empty area with the actual content, like in YouTube videos, where the video is recorded in portrait mode but postprocessed to match regular display (landscape mode). Just get the frame buffer, enlarge it (keeping correct aspect ratio), then blur it, render it in the window so it will cover the entire window (some parts will be cropped), then render the actual game frame centered in the window.

If you like, you can add support for maintaining correct aspect ratio when resizing but it would be good also to add the option to disable it (somewhere in the game options menu).

PS: the above should be considered only if the game always renders the game frame using fixed aspect ratio, or in other words, the field of view always has the same aspect ratio. If the field of view is not fixed, you should dynamically change it to adapt it to the current window aspect ratio and render the game frame so that it will fill the entire window client.

1 Like

Note that if I would like to handle controlling the size of the window during sizing, i would prefer to listen for windows messages, catch these messages:

  • WM_ENTERSIZEMOVE — send one time to the window, when moving/sizing starts,
  • WM_SIZING — send multiple times to the window, when its size changed,
  • WM_EXITSIZEMOVE — send one time to the window, finishing moving/sizing process.

and corrected the window size so that SDL now receives the corrected sizes and can send them in the SDL_WINDOWEVENT_RESIZED event (I think that’s how it works). I know that this is not a cross-platform solution but it should work excellent and similar things should be available on other platforms too.

If you will watch for the SDL_WINDOWEVENT_RESIZED event and correct the window size using the SDL_SetWindowSize, the window will constantly fight with the SDL, so as a result it will flicker, changing position back and forth. I did a simple test and unfortunately this is what it looks like, so using the SDL_SetWindowSize function is definitely a bad idea.

Listening for system messages and modifying their payload should be treated as a hypothesis for now, as I haven’t tested it yet.

Ok, I tested my hypothesis and yes, the method I described works great.

To be able to control the window proportions while stretching and at the same time avoid the window flickering problem, you need to watch system events and handle the WM_SIZING event. The win.lParam field of the event structure contains a pointer to the RECT structure, so to affect the size of the window that SDL will receive and that will be passed in the SDL_WINDOWEVENT_RESIZED event, you must modify the data of the structure pointed to by win.lParam.

There are a few things to keep in mind:

  • system events are disabled by default and are not queued by SDL, so to unlock them, use the SDL_EventState function.
  • queuing system events adds processing time overhead (by SDL and the application), so performance will suffer somewhat.
  • the above applies to the Windows platform, so a separate handler must be programmed for other platforms.

So, if you want to deliver a high-quality solution, you have to work a little harder. :wink:


what would be the cheapest way of doing the black space?

If one renderer is used, and is cleared with black each frame, the middle part is unnecessarily being blacked-out because it will be overwritten.

This could be mitigated by drawing two black rectangles. But even then one is updating a part of the screen that doesn’t really change.

So should there be multiple renderers or something like that? One being updated each frame in the middle, and the other behind, only updated on window events that may mess it up?

One could also make an argument that the user should be punished (with having to copy extra un-needed pixels) for making the window a weird size.

I don’t know. I always prefer to arrange stuff in such a way where it can perform fast without extra work.

The CPU will spend more time calculating the areas of these rectangles than the GPU will spend filling the entire window with color. So don’t mess around, because anyway, each frame of the game is rendered from scratch.

If just centering the frame image is sufficient, use the SDL_RenderSetLogicalSize function. If this function is not sufficient because you need more options, you can manually calculate the area to render the frame (just like me). In this case, first clear the window (fill with black), then render the frame in the calculated area.

If you need to, you can always use a back buffer in the form of SDL_Texture (or multiple ones and create a whole rendering chain), paint a frame on that buffer first, and then paint that buffer onto the window (in the computed area).

It all depends on your requirements.

SDL_RenderClear() is very fast, and it is recommended that you use it even if you are subsequently overwriting part (or all) of the frame. Don’t worry about the overhead, it should be insignificant.

I am also looking for an efficient way to keep the aspect ratio. Black borders or something similar are not an option. For now I do this after receiving SDL_EVENT_WINDOW_RESIZED (SDL3 pre-release):

void Screen_SizeChanged(void) {
	float scale;
	int h;

	SDL_GetWindowSize(sdlWindow, NULL, &h);
	scale = (float)h / height;
	SDL_SetWindowSize(sdlWindow, width*scale, height*scale);
	SDL_SetRenderScale(sdlRenderer, scale*dpiFactor, scale*dpiFactor);

This works on macOS (no flickering as described above) but seems to be a bit too complicated. Does anyone have a simpler way to keep aspect ratio?

Note: dpiFactor is a float that is set during initialisation and represents the amount of phyical pixels per logical pixel, width and height represent the logical size of the content.

Kind of overkill, but I solved a similar problem using Qt window - quite cross-platform and works great. Got some nice qt features as a bonus though.