Ok, I have prepared a comprehensive demo illustrating how I handle various window display modes and support rendering game frames on a fixed-size rear buffer, while simultaneously rendering this buffer on the screen maintaining the appropriate aspect ratio.
The demo is written in Free Pascal, because this is the language I use to program my engine (and everything else for desktops), but this language is so simple and similar to C that no one should have problems understanding the code in this demo. The demo is in single-file form and without any additional functions — just all the code in the main code block of the Pascal program (which is the same as the main
function in C). No platform-specific code, so this demo should work also on Linux, macOS and so on (I have Windows only, so I can’t test other platforms).
Release to play with (Win x64) — SDL Window Modes.zip (800.8 KB).
Source code for FPC/Lazarus — SDL Window Modes (source)…zip (993.5 KB) (with SDL headers from PascalGameDevelopment).
Keys:
W
— set the window to normal mode, bordered and stretchable.
M
— set the window as maximized on the current screen.
F
— set the window as fullscreen on the current screen (fake fullscreen).
E
— show window in the exclusive video mode on the primary screen.
Tab
— toggle the mouse grab feature.
Esc
— close the window.
When the window is displayed in the windowed mode:
A
— adjust the size of the window to its internal client area (without empty bars).
When the exclusive video mode is enabled:
PageUp
— set the video mode with the larger resolution.
PageDown
— set the video mode with the lower resolution.
Source code, with comments:
uses
SDL2;
const
// Available window styles supported by our game.
WINDOW_STYLE_WINDOWED = 0; // The window is displayed with a border, usually smaller than the screen area.
WINDOW_STYLE_MAXIMIZED = 1; // The window is displayed with a border, as maximized on the screen.
WINDOW_STYLE_FULLSCREEN = 2; // The window is displayed as a regular borderless window, stretched to the full screen.
WINDOW_STYLE_EXCLUSIVE = 3; // The window is displayed borderless, in exclusive video mode.
label
Cleanup; // Jump target to finalize resources (labels in C are not declared, so ignore it).
var
Window: PSDL_Window; // A handle to the SDL window.
Renderer: PSDL_Renderer; // A handle to the SDL renderer, used to paint both back buffer and window textures.
BackBuffer: PSDL_Texture; // A handle to the back buffer texture (to render game frame).
var
WindowFlags: UInt32; // A set of flags taken from the SDL window, when updating window style.
WindowStyle: UInt32; // The current window style (windowed, maximized, fullscreen or exclusive).
WindowClient: TSDL_Rect; // The current window client area (calculated every time the window changes size).
WindowMode: TSDL_DisplayMode; // Auxiliary variable, with the display mode to set.
WindowModeIndex: UInt32; // Auxiliary variable, with the index of the last used video mode.
WindowGrabMouse: Boolean; // A flag specifying whether the mouse is currently grabbed in the window.
var
Event: TSDL_Event; // Needed to process events from the SDL queue.
begin
// Initialize the SDL system. For the purposes of the test, there is no error handling and only the video and event
// subsystems are initialized (video initialization also initializes the event subsystem).
SDL_Init(SDL_INIT_VIDEO);
// Create a window and a renderer capable of changing the target, and create a back buffer texture intended to render a game
// frame at a specific, low resolution (for demo purposes, no error handling).
Window := SDL_CreateWindow ('Window modes demo', 0, 0, 0, 0, SDL_WINDOW_RESIZABLE or SDL_WINDOW_HIDDEN);
Renderer := SDL_CreateRenderer (Window, -1, SDL_RENDERER_ACCELERATED or SDL_RENDERER_PRESENTVSYNC or SDL_RENDERER_TARGETTEXTURE);
BackBuffer := SDL_CreateTexture (Renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, 400, 225);
// Initialize the window size and position. Normally this is not necessary, but for demo purposes, changing the window size
// generates the "SDL_WINDOWEVENT_SIZE_CHANGED" event, thanks to which the window client area will be recalculated in the
// first iteration of the main loop.
//
// In the target game code, you should separate the code for handling this event into a separate function and call it after
// creating the window. For demo purposes, the code in this file does not contain any functions.
SDL_SetWindowSize (Window, 800, 600); // The default window size can be any, choose your own resolution.
SDL_SetWindowPosition (Window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); // Center the window on the screen.
SDL_SetWindowMinimumSize (Window, 400, 225); // Not less than the resolution of the back buffer texture.
SDL_ShowWindow (Window); // Show window after setting its size and position.
// Set basic window parameters for loop operation and window modes.
WindowStyle := WINDOW_STYLE_WINDOWED; // By default the window is shown as 800×600.
WindowModeIndex := 0; // The default video mode is the first available, most likely the native one.
WindowGrabMouse := False; // By default the window does not grab the mouse (use "Tab" key to toggle it).
// Execute the game loop until the quit event occurs.
while True do
begin
// Process all queued events.
while SDL_PollEvent(@Event) = 1 do
case Event.Type_ of
// Window-related events.
SDL_WINDOWEVENT:
case Event.Window.Event of
// The window size has changed — this could be resizing the window by stretching it with the mouse, or changing its
// style to maximized, fullscreen or exclusive. Here is where the window client area must be resized to fit it.
SDL_WINDOWEVENT_SIZE_CHANGED:
begin
// First, try to determine the client area by the width of the window. The height may be different than the new
// window height, so there will be empty bars above and below the client area.
WindowClient.W := Event.Window.Data1;
WindowClient.H := Round(Event.Window.Data1 * (225 / 400)); // Win.W * (Buff.H / Buff.W) — this is important.
// Check that the height of the client area is not greater than the height of the entire window. If it is larger,
// this time we need to adjust the client's area in relation to the window height.
if WindowClient.H > Event.Window.Data2 then
begin
// Calculate the window client area based on the window height. Here it is certain that the area will fit in the
// window, its height will be consistent with the height of the window, and there will be empty bars on the sides.
WindowClient.H := Event.Window.Data2;
WindowClient.W := Round(Event.Window.Data2 * (400 / 225)); // Win.H * (Buff.W / Buff.H) — this is important.
end;
// The client's size is calculated, so center it in the window.
WindowClient.X := (Event.Window.Data1 - WindowClient.W) div 2;
WindowClient.Y := (Event.Window.Data2 - WindowClient.H) div 2;
// If grabbing the mouse is active, update the mouse rect.
if WindowGrabMouse then
SDL_SetWindowMouseRect(Window, @WindowClient);
end;
// The window was maximized.
SDL_WINDOWEVENT_MAXIMIZED:
// The window has been maximized (e.g. with a button on the bar), so the current style is maximized.
WindowStyle := WINDOW_STYLE_MAXIMIZED;
// The window was restored.
SDL_WINDOWEVENT_RESTORED:
// If a window was restored from a maximized state, it is now a small window, so the current style is windowed.
if WindowStyle = WINDOW_STYLE_MAXIMIZED then
WindowStyle := WINDOW_STYLE_WINDOWED;
end;
SDL_KEYDOWN:
// Only perform the action if it is a fresh keypress.
if Event.Key.Repeat_ = 0 then
case Event.Key.KeySym.Scancode of
// "A" key — adjust the window size to the proportions of the back buffer.
SDL_SCANCODE_A:
// Do it only if the window is shown in the windowed mode.
if WindowStyle = WINDOW_STYLE_WINDOWED then
// The client area is calculated, so use it to set the size of the window.
SDL_SetWindowSize(Window, WindowClient.W, WindowClient.H);
// "W" key — show window in the windowed mode.
SDL_SCANCODE_W:
// Update the window style only if a different style is currently set.
if WindowStyle <> WINDOW_STYLE_WINDOWED then
begin
// Set the window style to windowed and get the current window flags to disable some features.
WindowStyle := WINDOW_STYLE_WINDOWED;
WindowFlags := SDL_GetWindowFlags(Window);
// if the window is maximized or fullscreen, disable these modes.
if WindowFlags and SDL_WINDOW_FULLSCREEN <> 0 then SDL_SetWindowFullscreen (Window, 0);
if WindowFlags and SDL_WINDOW_FULLSCREEN_DESKTOP <> 0 then SDL_SetWindowFullscreen (Window, 0);
if WindowFlags and SDL_WINDOW_MAXIMIZED <> 0 then SDL_RestoreWindow (Window);
// Set the default window size and center in on the screen.
SDL_SetWindowSize (Window, 800, 600);
SDL_SetWindowPosition (Window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
end;
// "M" key — show window as maximized.
SDL_SCANCODE_M:
// Update the window style only if a different style is currently set.
if WindowStyle <> WINDOW_STYLE_MAXIMIZED then
begin
// Set the window style to maximized and get the current window flags to disable some features.
WindowStyle := WINDOW_STYLE_MAXIMIZED;
WindowFlags := SDL_GetWindowFlags(Window);
// If the window is fullscreen, disable it.
if WindowFlags and SDL_WINDOW_FULLSCREEN <> 0 then SDL_SetWindowFullscreen (Window, 0);
if WindowFlags and SDL_WINDOW_FULLSCREEN_DESKTOP <> 0 then SDL_SetWindowFullscreen (Window, 0);
// Restore the default window size, center the window on the screen and maximize it.
SDL_SetWindowSize (Window, 800, 600);
SDL_SetWindowPosition (Window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
SDL_MaximizeWindow (Window);
end;
// "F" key — show window in the fake fullscreen mode.
SDL_SCANCODE_F:
// Update the window style only if a different style is currently set.
if WindowStyle <> WINDOW_STYLE_FULLSCREEN then
begin
// Set the window style to fullscreen and get the current window flags to disable some features.
WindowStyle := WINDOW_STYLE_FULLSCREEN;
WindowFlags := SDL_GetWindowFlags(Window);
// If the window is maximized or fullscreen, disable these features.
if WindowFlags and SDL_WINDOW_FULLSCREEN <> 0 then SDL_SetWindowFullscreen (Window, 0);
if WindowFlags and SDL_WINDOW_MAXIMIZED <> 0 then SDL_RestoreWindow (Window);
// Restore the default window size, center the window on the screen and enable fake fullscreen.
SDL_SetWindowSize (Window, 800, 600);
SDL_SetWindowPosition (Window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
SDL_SetWindowFullscreen (Window, SDL_WINDOW_FULLSCREEN_DESKTOP);
end;
// "E" key — show window in the exclusive video mode.
SDL_SCANCODE_E:
// Update the window style only if a different style is currently set.
if WindowStyle <> WINDOW_STYLE_EXCLUSIVE then
begin
// Set the window style to exclusive and get the current window flags to disable some features.
WindowStyle := WINDOW_STYLE_EXCLUSIVE;
WindowFlags := SDL_GetWindowFlags(Window);
// Restore the window to the windowed mode, if it is maximized or in fake fullscreen mode.
if WindowFlags and SDL_WINDOW_FULLSCREEN_DESKTOP <> 0 then SDL_SetWindowFullscreen (Window, 0);
if WindowFlags and SDL_WINDOW_MAXIMIZED <> 0 then SDL_RestoreWindow (Window);
// Get information about the video mode supported by the display driver, based on the current index. This index
// initially points to the first video mode, but later if you change to a lower resolution mode, its index will be
// remembered. So if you later turn on the exclusive video mode again, the last used one will be set. Finally, set
// the window video mode and enable exclusive video mode.
SDL_GetDisplayMode (0, WindowModeIndex, @WindowMode);
SDL_SetWindowDisplayMode (Window, @WindowMode);
SDL_SetWindowFullscreen (Window, SDL_WINDOW_FULLSCREEN);
end;
// "PageUp" key — set next video mode supported by the display driver (larger resolution).
SDL_SCANCODE_PAGEUP:
// If the exclusive video mode is active and there is supported video mode with the larger resolution, use it.
if (WindowStyle = WINDOW_STYLE_EXCLUSIVE) and (WindowModeIndex > 0) then
begin
// Get the video mode with the larger resolution and its data.
WindowModeIndex -= 1;
SDL_GetDisplayMode(0, WindowModeIndex, @WindowMode);
// Disable exclusive video mode before setting a new window mode. I don't know why this is necessary, but without
// it, the resolution will be set incorrectly. So turn off exclusive video mode, set a new window mode and turn on
// video mode again.
SDL_SetWindowFullscreen (Window, 0);
SDL_SetWindowDisplayMode (Window, @WindowMode);
SDL_SetWindowFullscreen (Window, SDL_WINDOW_FULLSCREEN);
end;
// "PageDown" key — set previous video mode supported by the display driver (lower resolution).
SDL_SCANCODE_PAGEDOWN:
//
if (WindowStyle = WINDOW_STYLE_EXCLUSIVE) and (WindowModeIndex < SDL_GetNumDisplayModes(0) - 1) then
begin
// Get the video mode with the lower resolution and its data.
WindowModeIndex += 1;
SDL_GetDisplayMode(0, WindowModeIndex, @WindowMode);
// Disable exclusive video mode before setting a new window mode. I don't know why this is necessary, but without
// it, the resolution will be set incorrectly. So turn off exclusive video mode, set a new window mode and turn on
// video mode again.
SDL_SetWindowFullscreen (Window, 0);
SDL_SetWindowDisplayMode (Window, @WindowMode);
SDL_SetWindowFullscreen (Window, SDL_WINDOW_FULLSCREEN);
end;
// "Tab" key — toggle grabbing the mouse.
SDL_SCANCODE_TAB:
begin
// Toggle flag specifying whether the mouse cursor should be grabbed.
WindowGrabMouse := not WindowGrabMouse;
// Don't forget "SDL_SetWindowGrab". Otherwise, grabbing will not work in the exclusive video mode.
if WindowGrabMouse then
begin
// If the mouse is grabbed, set its area to match the calculated area of the window client (the area of the
// window where the back buffer with the contents of the game frame will be rendered).
SDL_SetWindowMouseRect (Window, @WindowClient);
SDL_SetWindowGrab (Window, SDL_TRUE);
end
else
begin
// The mouse is not grabbed, so reset the mouse rect and turn off grab mode.
SDL_SetWindowMouseRect (Window, nil);
SDL_SetWindowGrab (Window, SDL_FALSE);
end;
end;
// Break the main loop and jump directly to resource finalization.
SDL_SCANCODE_ESCAPE: goto Cleanup;
end;
// Return from the loop and clean up resources.
SDL_QUITEV: goto Cleanup;
end;
// Here is where you should render the game frame to the back buffer. This buffer always has a predetermined size (in your
// case it is 400×225) and it does not matter what size the window is currently or its calculated client area. Just render
// the frame in the back buffer texture (cover its entire area). For demo purposes, the buffer is filled with gray color.
SDL_SetRenderTarget (Renderer, BackBuffer);
SDL_SetRenderDrawColor (Renderer, 100, 100, 100, 255);
SDL_RenderClear (Renderer);
// We restore the renderer's target to the window texture. Here you need to clear the entire window area so that the empty
// areas on the sides of the calculated client area do not contain junk pixel colors.
//
// If you enable exclusive video mode with a resolution that does not match the native screen resolution, the display
// driver will most likely fill the rest of the screen with black. By filling the entire window area in dark gray, you
// will be able to see what fills the display driver, what belongs to the window but is outside the client, and what is
// the client.
//
// If the area of the window client is smaller than the area of the entire window, you can calculate the areas of the
// empty bars (always two) and render something on them, e.g. tiles with some pattern.
SDL_SetRenderTarget (Renderer, nil);
SDL_SetRenderDrawColor (Renderer, 32, 32, 32, 255);
SDL_RenderClear (Renderer);
// The entire window area is painted, the back texture has the game frame rendered, so the last step is to render the
// entire buffer texture in the calculated client area (with stretching) and flip the window texture onto the screen.
SDL_RenderCopy (Renderer, BackBuffer, nil, @WindowClient);
SDL_RenderPresent (Renderer);
end;
Cleanup:
// Destroy all resources used and close the SDL system.
SDL_DestroyTexture (BackBuffer);
SDL_DestroyRenderer (Renderer);
SDL_DestroyWindow (Window);
SDL_Quit();
end.
Enjoy.