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.
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.
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
SDL_Window * window;
SDL_Renderer * renderer;
SDL_Texture * backBuffer;
SDL_Texture * ballImage;
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
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
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)
bool quit = false;
quit = HandleEvents();
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
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.
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.
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!",
//You must use the SDL_RENDERER_TARGETTEXTURE flag in order to target the backbuffer
renderer = SDL_CreateRenderer(window,
//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,
//IMPORTANT Set the back buffer as the target
//Load an image yay
SDL_Surface * image = SDL_LoadBMP("Ball.bmp");
ballImage = SDL_CreateTextureFromSurface(renderer, 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.
if(event.type == SDL_QUIT)
else if(event.type == SDL_WINDOWEVENT)
if(event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
resize = true;
function for clarification. Resize
is not called all the time, just when the window's size changes.
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
backBuffer = SDL_CreateTexture(renderer,
SDL_TEXTUREACCESS_TARGET, //Again, must be created using this
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.
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.
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 = false;
SDL_RenderCopy(renderer, backBuffer, &nativeSize, &newWindowSize); //Render the backBuffer onto the
//screen at (0,0)
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.
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: