Stretching Your Game To Fit The Screen Without Letterboxing - SDL2

Published January 27, 2014 by Alex Walters, posted by AlexWalters22
Do you see issues with this article? Let us know.
Advertisement
If you're making a game that you want to be used on a wide variety of systems, your game has to be scalable, especially if it is in a very small resolution. This is usually pretty straightforward. Most graphics libraries and the like have things built in for doing the scaling for you. SDL2 does, but only to a certain extent. If you want your game to be scaled without letter-boxing, you have to write this yourself. It took me a while to figure out, so I though I would spread the knowledge.
Using this form of scaling can create some serious distortion as well as introduce scaling artifacts to the game with different aspect ratios.
The code included is written in C++ and uses SDL2, which can be found here: http://libsdl.org/. The documentation which describes the functions that I use is located here: http://wiki.libsdl.org/CategoryRender. And to set up SDL2, some easy instructions can be found at the awesome site LazyFoo

Scaling On Up

The theory here is this: You don't want letterboxing. You want the game to be taken from its original size and stretched to fit the window no matter what. So you just render everything to a back buffer by setting the target to it, instead of to the window's buffer, then at the end of your rendering cycle, switch the target back to the screen and RenderCopy the back buffer onto the screen, rescaling if neccessary, then set the target back to your back buffer and repeat. It's slightly trickier than it sounds.

Enter The Code

So in order to accomplish scaling without letter-boxing, we will need some SDL rectangles variables. SDL_Rect nativeSize; SDL_Rect newWindowSize; One is for storing the original size of the game, nativeSize, and one is for storing the size of the window when it changes size, newWindowSize. float scaleRatioW; float scaleRatioH; SDL_Window * window; SDL_Renderer * renderer; SDL_Texture * backBuffer; SDL_Texture * ballImage; bool resize; We also will need an SDL_Window, an SDL_Renderer for that window, and last but not least a backBuffer for rendering everything to before copying it, properly scaled, to the window's buffer. Also there is ballImage which will be used to demonstrate the actual scaling. The two floats, scaleRatioW and scaleRatioH are optional. They are used if you need to scale the values of certain things that rely on the coordinates of the mouse, e.g. a button. You would multiply the x coord and width of its bounding-box by scaleRatioW and the y coord and height by scaleRatioH. resize is used to tell the render function to call the resize function. It is neccessary to do it this way because there is a stall in the program after resizing the window that can sometimes cause problems if you call the resize functions immediately upon receiving a resize event. So I have set up a fairly simple main function to run the program. int main(int argc, char * argv[]) { InitValues(); InitSDL(); bool quit = false; while(!quit) { quit = HandleEvents(); Render(); } return 0; } InitValues does what you might expect it to do, gives all the variables their starting values. InitSDL does most of the importnat work, setting up the window and renderer as well as set up the back buffer and load the ball image. HandleEvents returns false if the user clicks the x button on the window, however it also captures the window resize event and resizes the screen using a function called Resize. Render handles a very important part of the process, changing from the back buffer to the window buffer. I'll explain each of the functions in turn. void InitValues() { nativeSize.x = 0; nativeSize.y = 0; nativeSize.w = 256; nativeSize.h = 224; newWindowSize.x = 0; newWindowSize.y = 0; newWindowSize.w = nativeSize.w; newWindowSize.h = nativeSize.h; scaleRatioW = 1.0f; scaleRatioH = 1.0f; window = NULL; renderer = NULL; backBuffer = NULL; ballImage = NULL; resize = false; } I set the native size to that of the SNES, 256 by 224. void InitSDL() { if(SDL_Init(SDL_INIT_EVERYTHING) < 0) { cout << "Failed to initialize SDL" << endl; } //Set the scaling quality to nearest-pixel if(SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0") < 0) { cout << "Failed to set Render Scale Quality" << endl; } //Window needs to be resizable window = SDL_CreateWindow("Rescaling Windows!", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, nativeSize.w, nativeSize.h, SDL_WINDOW_RESIZABLE); //You must use the SDL_RENDERER_TARGETTEXTURE flag in order to target the backbuffer renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE); //Set to blue so it's noticeable if it doesn't do right. SDL_SetRenderDrawColor(renderer, 0, 0, 200, 255); //Similarly, you must use SDL_TEXTUREACCESS_TARGET when you create the texture backBuffer = SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), SDL_TEXTUREACCESS_TARGET, nativeSize.w, nativeSize.h); //IMPORTANT Set the back buffer as the target SDL_SetRenderTarget(renderer, backBuffer); //Load an image yay SDL_Surface * image = SDL_LoadBMP("Ball.bmp"); ballImage = SDL_CreateTextureFromSurface(renderer, image); SDL_FreeSurface(image); } This is a fairly large chunk of code. First SDL_Init is called to set up all the SDL subsystems. Next, something that one might find important, SDL_SetHint is called to set the render scale quality to nearest-pixel. Linear quality makes scaling very small images to very large images hazy, and this is unwanted behavior. Next the window is created with the SDL_WINDOW_RESIZABLE flag to allow it to be resized. Then, very important, the renderer is created with the SDL_RENDERER_TARGETTEXTURE flag which allows a texture to be targeted, and backBuffer must be created using the SDL_TEXTUREACCESS_TARGET flag for it to be used as a back buffer. The render target is then set to backBuffer so that all drawing will happen on it, not on the window. bool HandleEvents() { while(SDL_PollEvent(&event) ) { if(event.type == SDL_QUIT) { return true; } else if(event.type == SDL_WINDOWEVENT) { if(event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { resize = true; } } } return false; } The HandleEvents function for clarification. Resize is not called all the time, just when the window's size changes. void Resize() { int w, h; SDL_GetWindowSize(window, &w, &h); scaleRatioW = w / nativeSize.w; scaleRatioH = h / nativeSize.h; //The ratio from the native size to the new size newWindowSize.w = w; newWindowSize.h = h; //In order to do a resize, you must destroy the back buffer. Try without it, it doesn't work SDL_DestroyTexture(backBuffer); backBuffer = SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), SDL_TEXTUREACCESS_TARGET, //Again, must be created using this nativeSize.w, nativeSize.h); SDL_Rect viewPort; SDL_RenderGetViewport(renderer, &viewPort); if(viewPort.w != newWindowSize.w || viewPort.h != newWindowSize.h) { //VERY IMPORTANT - Change the viewport over to the new size. It doesn't do this for you. SDL_RenderSetViewport(renderer, &newWindowSize); } } So here is the most important part of scaling and where it became tricky. There are quite a few things that happen here. First, get the new window size (using GetWindowSize for clarity). Then calculate the new ratio based on the new size and the native size. Then, Very Important you must destory and recreate your back buffer. I honestly don't know why, but if you try without doing this it will not work. Then, also Very Important, you must set the view port to the new window size because it will not do it for you, or do it right anyways. void Render() { SDL_RenderCopy(renderer, ballImage, NULL, NULL); //Render the entire ballImage to the backBuffer at (0, 0) SDL_SetRenderTarget(renderer, NULL); //Set the target back to the window if(resize) //If a resize is neccessary, do so. { Resize(); resize = false; } SDL_RenderCopy(renderer, backBuffer, &nativeSize, &newWindowSize); //Render the backBuffer onto the //screen at (0,0) SDL_RenderPresent(renderer); SDL_RenderClear(renderer); //Clear the window buffer SDL_SetRenderTarget(renderer, backBuffer); //Set the target back to the back buffer SDL_RenderClear(renderer); //Clear the back buffer } And last but not least the Render function. The comments explain this one pretty well.
For some reason, whenever you first resize the window, there is about a three-second delay and after that it resizes without a hitch. Not sure why on that one.

Conclusion

SDL2 is a pretty straight-forward API for doing just about anything. However, getting around letter-boxing was slightly tricky. Some of the things that occur in the background of SDL aren't obvious which led to a sort of trial and error process to figure out the way around. I hope this helps if anyone has the same problem with letter-boxing that I did. Here is the source. You can run it from inside the bin folder: SDL2StrechToScreen.zip
Cancel Save
0 Likes 3 Comments

Comments

Ravyne

Some feedback:

While this is an article about how to stretch your target resolution to native resolution "no matter what" to avoid letterboxing, it still probably warrants some discusion regarding the pros and cons of both approaches.

Specifically, nowhere that I see does it state the the primary drawback of blindly stretching to full screen is that some rather wild distortion can be introduced when you disallow letterboxing. That 256x240 target resolution might not look so bad on an old 1280x1024 (5:4) lcd panel, but it looks a bit weird on common old 4:3 screens, and really weird on the 16:9 and 16:10 screens predominant today.

Also you don't discus the other primary drawback of blindly filling the full screen, which is to introduce scaling artifacts. In many kinds of games, the scaling artifacts may not be an issue. But they definately are an issue for the retro-styled, pixel-perfect 2D games that are most likely to implement the kind of resolution-fixing scaling you are prescribing (regardless of whether they accept letter boxing or not).

Finally, 256x224 is not the resolution of the gameboy (that gameboy was 144x160), but the standard display resolution of the SNES.

January 27, 2014 10:47 PM
EDI
EDI

Blindly scaling where there is a difference in aspect ratio is a pretty bad idea.

To maintain aspect ratio; you need to either letterbox, or clip.

January 28, 2014 04:29 PM
AlexWalters22

Thanks for the feed back guys(Ravyne and EDI). FIrst of all, I will fix the 256x224 resolution to say SNES (woops). I understand that it is probably a bad idea in most situations to scale to fit the screen no matter what and it definitely produces scaling artifacts and distortion. I'll add a disclaimer at the top of this article identifying those drawbacks. For the most part, the reason that I created the article is that it was suprisingly tricky to figure out the rights steps to take to actually get rid of letter-boxing if one wanted to. It is a fairly simple (albeit crude) way of supporting any size thrown at it without letter-boxing it. It is definitely a temporary fix and not a long-term solution. I thought I would show how I did it after reading this:

http://comments.gmane.org/gmane.comp.lib.sdl/61628

January 29, 2014 04:55 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

Have you tried scaling your game using the SDL_RenderSetLogicalSize or the SDL_RenderSetScale functions and gotten the ugly letter-boxing? This shows how you can just scale your game no matter what without letter-boxing using SDL2.

Advertisement
Advertisement

Other Tutorials by AlexWalters22

AlexWalters22 has not posted any other tutorials. Encourage them to write more!
Advertisement