Is there a way to achieve parity between Windows and Linux with Fullscreen behavior?

Hi Folks,

I have a program that renders in 400x225 resolution (trying to achieve the same aspect ratio as 1920x1080 but much lower res) in full screen using hardware accelerated rendering and an SDL_Texture as the framebuffer, copying it to the renderer each frame. I’m using clang++ currently and my renderer init code looks like this:

window = SDL_CreateWindow("3D Engine", HALF_SCREEN_W,HALF_SCREEN_H, SCREEN_W, SCREEN_H, screen_mode);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

Windows build
When I build this program in Windows, I get a correct looking full screen image:

Linux build
When I build this program in Linux and run, I get a picture like this:


as I write this post, I’m realizing the linux fullscreen might be also forcing my aspect ratio into something like 4:3?

After doing some research, I wonder if perhaps this is just a difference between linux and windows, or perhaps a driver issue. Is there anything I can do to make it fullscreen stretched in both environments using this type of renderer?

If it helps, my linux stack is Manjaro Linux running inside Oracle Virtual Box, though I see similar behavior when I try to run this on my steam deck using Manjaro booted off an SD card.

I will tell you how I do it, but there are many solutions.

In my engine, the frame is rendered on the back buffer in the form of SDL_Texture, with a size of 358×224 pixels. The window can be any size, so in order to maintain the correct frame proportions on the screen, I calculate the target area myself—the area of the window client where the frame texture will be rendered.

If the aspect ratio of the frame buffer is different than the aspect ratio of the window, the target area may be lower or narrower than the window client area. When rendering an image in a window, I first fill the entire window client with black and then use SDL_RenderCopy to render the texture in the calculated area.

The target area can be calculated on every frame of the game, but it can also only be calculated when the SDL_WINDOWEVENT_RESIZED event is received (I prefer the second).


As for other methods, you can also play with logical size, or enumerate the available video mode resolutions and choose the one that has the aspect ratio closest to the aspect ratio of the frame buffer.

However, what I wrote above and what I use allows to always render a frame maintaining specific proportions, regardless of the resolution of the window and what mode it is in (windowed, fullscreen or exclusive video mode).

Are you using SDL_RenderSetLogicalSize ?

I think my render process is similar to what you described, but I don’t know that I have SDL_WINDOWEVENT_RESIZED anywhere. Essentially do something like this (in addition to the renderer init code I provided above):

// use DisplayMode to set the renderer to use 400x225 full screen?
SDL_DisplayMode mode; // I populate this using SDL_GetDisplayMode() initially
mode.h = SCREEN_H;
mode.w = SCREEN_W;
SDL_SetWindowDisplayMode(window, &mode);
SDL_SetWindowFullscreen(window, SDL_TRUE);

// Initialize the framebuffer as an SDL_Texture*
int tex_w, tex_h;
uint8_t *framebufferpixels=NULL;
int pitch=0; // size of one row in bytes
SDL_Texture *texture=SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB888,SDL_TEXTUREACCESS_STREAMING, SCREEN_W, SCREEN_H);
SDL_PixelFormat *pixelFormat = SDL_AllocFormat(SDL_PIXELFORMAT_RGB888);
SDL_QueryTexture(texture, &textureFormat, NULL, &tex_w, &tex_h);


while(game_running){
   //Run this code this every frame
   SDL_LockTexture(texture, NULL, (void **)&framebufferpixels, pitch)

   // use SDL_MapRGB() to write colors directly to framebuffer as needed for the frame

   SDL_UnlockTexture(texture);
   SDL_RenderCopy(renderer, texture, NULL, NULL);
   SDL_RenderPresent(renderer);
   
}

When you say you calculate the render area of the window target where the texture will be rendered, what does that mean exactly? If I passed in 400 and 225 as the width and height when initializing the renderer, and set the renderer to fullscreen (as I think I’ve done), wouldn’t that make the window area automatically 400 x 225? If not, what would I want to change here to ensure my SDL_Texture stretches across the renderer target? Would the code change still work in Windows?

Thanks

I wasn’t before, but just added in the following command just after SDL_SetWindowFullScreen()

SDL_RenderSetLogicalSize(renderer, SCREEN_W, SCREEN_H);

It appears to have fixed the aspect ratio issue where I thought it looked like 4:3 before, but it’s still centered in the middle of the screen and small. I still need to figure out how to stretch this to the full window size.

Linux build


Aspect Ratio is fixed, but still needs to be stretched.

Thanks

Here’s what I’d try: just before calling SDL_SetFullscreen(), destroy the current renderer and then
recreate it after calling SDL_SetFullscreen().

SDL_DestroyRenderer(renderer);
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC);

That should clear out any “states” that might be affecting the renderer.

Also, what fullscreen mode are you calling, SDL_WINDOW_FULLSCREEN, or SDL_WINDOW_FULLSCREEN_DESKTOP?

Edit:
… Wait! I think I found your issue, don’t use SDL_TRUE as a flag in SDL_SetWindowFullscreen, in SDL2 it’s supposed to be a window flag telling SDL what type of fullscreen mode to adopt, or Zero for window mode. → SDL2/SDL_SetWindowFullscreen - SDL Wiki
(For future readers: in SDL3 the function does expect SDL_Bool, but not in SDL2)

And now you may want to try it without the SDL_RenderSetLogicalSize() and displayMode functions, because those will also affect the fullscreen render.

In the structure representing the game window, I store the data of the area intended for rendering the frame. I simply have a field in the window structure that stores the current area where the frame is to be rendered. I recalculate this area while processing the GAME_SDL_EVENT_WINDOW_CHANGED_SIZE event — this event provides the new height and width of the window.

Additionally, in the window structure, I store the current frame aspect ratio in the form of two numbers - X and Y, which are simply the dimensions of the back buffer (i.e. 358 and 224, respectively).

After creating the window and each time the GAME_SDL_EVENT_WINDOW_CHANGED_SIZE event is processed, I call a function that recalculates the area for the frame, based on the new window dimensions and the set aspect ratios (these X and Y fields, which are by default the dimensions of the back buffer). The calculations are very simple, for me it looks like this, with comments (this is Pascal, but it doesn’t matter):

procedure Game_WindowSetClientRatio(AX, AY: TGame_SInt32);
var
  WindowWidth:  TGame_SInt32;
  WindowHeight: TGame_SInt32;
  ClientWidth:  TGame_SInt32;
  ClientHeight: TGame_SInt32;
begin
  // Update the fields for the new aspect ratio.
  Window.ClientRatioX := AX;
  Window.ClientRatioY := AY;

  // Get the current size directly from the SDL window. This is very important because if the window has changed size,
  // events related to it may not have been processed by the "Game_WindowUpdate" function yet.
  Game_WindowGetWH(@WindowWidth, @WindowHeight);

  // Calculate the aspect ratio of the window for the given window width. If the area fits within the window, it means that
  // black bars above and below the client area of the window will be rendered.
  ClientWidth  := WindowWidth;
  ClientHeight := Round(ClientWidth * (AY / AX));

  // If the area does not fit in the window, use the current height and calculate the area again. Here at 100% the area
  // will fit within the window and black bars will be rendered on the left and right side of the client area.
  if ClientHeight > WindowHeight then
  begin
    ClientHeight := WindowHeight;
    ClientWidth  := Round(ClientHeight * (AX / AY));
  end;

  // Update the client area position and size by centering the target frame area in the window.
  Window.RectClient.W := ClientWidth;
  Window.RectClient.H := ClientHeight;
  Window.RectClient.X := (WindowWidth  - ClientWidth)  div 2;
  Window.RectClient.Y := (WindowHeight - ClientHeight) div 2;

  // If the mouse cursor is grabbed by the window (it cannot be moved outside the window) but the virtual window stick is
  // not active, update the area available for the cursor to match the new internal client area.
  if Game_WindowGetInputAttached() and not Game_StickGetActive(Game_WindowStickGet()) then
    Game_SDL_WindowSetMouseRect(Window.HandleWindow, @Window.RectClient);
end;

The AX and AY parameters can be any positive value — I’m using the dimensions of the back buffer, but you can use any values, for example 4 and 3 (4:3 ratio), 16 and 10 (16:10 ratio) and so on.

Window is the local variable (local for the unit) that stores all window data (handles, rects, ratios, some additional flags etc.). It’s ClientRatioX and ClientRatioY stores the current aspect ratio factors. The RectClient field stores the area dedicated to render the frame. The current window sizes are retrieved using SDL functions to ensure that they are actually up to date (this is done in the Game_WindowGetWH function).

The above function is called in the window constructor function and in the SDL event processing function, but it can also be used in any other place, e.g. in the function updating the game logic (so that the player can change the image proportions in the game settings).

The calculations performed by it are not sensitive to the set proportion values or the window proportions. If the window’s aspect ratio is different than the frame’s specified aspect ratio, the frame rendering area will be smaller than the window’s area, resulting in empty stripes either on the sides or at the top and bottom. You can color these stripes arbitrarily by clearing the entire window area with SDL_RenderClear and then rendering the back buffer in the calculated area.


PS: Pascal syntax is not highlighed, so I didn’t specify any syntax (the forum parser somehow colored this code itself, a bit wrong, but better than nothing).

Thanks for the code, and description - I’ll try to identify my question in bold so they don’t get lost in my post.

I’m still not sure I understand what the Window structure is. Is Window a custom defined datatype you created? I wasn’t able to locate attributes like ClientRatioX or ClientRatioY in the SDL documentation. Reading through your documentation I have a few other questions:

  • Am I right to understand that inside Game_WindowGetWH() you call SDL_GetWindowSize() to populate values for WindowWidth and WindowHeight?
  • What is the difference between Window, Client (and ‘target’ from your previous post)? Would any of these be the same thing as SDL_Texture* texture that I use in my code and treat as the frame buffer with a desired resolution/aspect ration that I draw to and copy to the renderer?
  • What part of this code actually changes how the screen is drawn (I don’t see calls to things like SDL_SetWindowSize())?
  • Is Window actually a SDL_Window* in your code? I’m trying to figure out how you are directly manipulating the handles, rects, ratios, etc that you mentioned in your post without calling any SDL functions (aside from Game_SDL_WindowSetMouseRect(), which I think might be a wrapper for SDL_WindowSetMouseRect()).

Conceptually, I am trying to have the size and aspect ratio (400x225) of my SDL_Texture scale to full screen regardless of it’s resolution (I’m fine with letterboxes if the aspect ratio doesn’t line up). I did play around with some of the other functions that were mentioned and wanted to identify something interesting that I noted. I added this code to cycle through the various resolutions that I have:

	static int display_in_use = 0;
	int i, display_mode_count;
	SDL_DisplayMode mode;
	Uint32 f;
	display_mode_count = SDL_GetNumDisplayModes(display_in_use);
	for (i = 0; i < display_mode_count; ++i) {
		SDL_GetDisplayMode(display_in_use, i, &mode);
		f = mode.format;
		std::cout << "Display mode: " << i << "   BPP: " << SDL_BITSPERPIXEL(f) << " "<< SDL_GetPixelFormatName(f) << " Res: " << mode.w << "x" << mode.h << std::endl;
	}

The output for this is as follows:

Display mode: 0   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 3840x2400
Display mode: 1   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 3840x2160
Display mode: 2   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 2880x1800
Display mode: 3   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 2560x1600
Display mode: 4   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 2560x1440
Display mode: 5   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1920x1440
Display mode: 6   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1920x1200
Display mode: 7   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1920x1080
Display mode: 8   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1856x1392
Display mode: 9   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1792x1344
Display mode: 10   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1680x1050
Display mode: 11   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1600x1200
Display mode: 12   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1440x900
Display mode: 13   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1400x1050
Display mode: 14   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1360x768
Display mode: 15   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1280x1024
Display mode: 16   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1280x960
Display mode: 17   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1280x800
Display mode: 18   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1280x768
Display mode: 19   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1280x720
Display mode: 20   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1152x864
Display mode: 21   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 1024x768
Display mode: 22   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 800x600
Display mode: 23   BPP: 24 SDL_PIXELFORMAT_RGB888 Res: 640x480

I also noticed that when trying to create a window using SDL_CreateWindow with 400 and 225 as the width and height values, or try to do the same thing with SDL_SetWindowSize(), when I turn around and query the window resolution using SDL_GetWindow_Size(), I always get 640x480. This leads me to the last question I can think of right now: Do I have to set the resolution of my window to one of the display mode values?

Thanks!

Yes — it is simple structure which contain all data about the window, that I need. It’s declared like this:

type
  // The structure of the game window object.
  TGame_Window = object void
    HandleWindow:   PGame_SDL_Window; // Internal SDL window handle.
    HandleRenderer: PGame_SDL_Renderer; // The internal handle of the SDL renderer assigned to the window.
    StyleCurrent:   TGame_SInt32; // The current style of the displayed window.
    StyleWindowed:  TGame_SInt32; // The window style when displayed in non-exclusive mode (windowed, maximized or fullscreen).
    ClientRatioX:   TGame_SInt32; // The X factor of the window client area that is intended to render the game frames (without black bars).
    ClientRatioY:   TGame_SInt32; // The Y factor of the window client area that is intended to render the game frames (without black bars).
    RectWindowed:   TGame_RectS32; // The window client area when displayed in non-exclusive mode.
    RectClient:     TGame_RectS32; // Current window client area dedicated to rendering game frames.
    InputAttached:  TGame_Bool8; // A flag indicating whether the mouse and keyboard are grabbed by the window.
    Closed:         TGame_Bool8; // A flag indicating whether an attempt was made to close the window.
  end;

So all its data is created by me, for my purposes. Focus on the RectClient field and the ClientRatioX and ClientRatioY fields — they are important in your case. Just calculate the RectClient using the ClientRatioX and ClientRatioY and the current size of the window (when processing SDL events), and then use the RectClient when rendering the window.

Yes — I note that in my previous post.

I just used other word to describe the area dedicated to render the frame buffer in. If you need to know, there are difference:

  • RectWindowed — contains the area of the entire window if the window is not fullscreen. This applies to a situation where the window is smaller than the screen and has a system border. I need it to be able to remember the last window size I set and be able to restore it when I restart the game and also when I turn off the exclusive video mode. Don’t worry about it, it is not needed for frame buffer rendering.

  • RectClient — since my engine considers the window client area to be an area with the given aspect ratio (compatible with the back buffer), this field is used to store the area where the back buffer texture is rendered. Additionally, it also determines the area in which the mouse can move if mouse is grabbed by the window. This is the field you should be interested in.

No no no — back buffer should be declared and filled somwhere else, because it is not related to the window itself. So declare a back buffer texture, render the game frame content in it, and when frame rendering is finished, render it in the window, within the calculated area (RectClient field).

I showed you how to calculate the target area of the window where the frame texture should be rendered. I thought you’d be able to get through it with a ready-made rendering area. But no problem, see what the sample code for rendering the buffer texture in this area looks like (in a simplified form, to be clear):

// Restore the default renderer target, which is just the window.
SDL_SetRenderTarget    (Window.HandleRenderer, nil);

// Fill the entire window area with black. If the area to render the back buffer texture is smaller that the window 
// itself (it has different aspect ratio than the window), this is where the empty space is filled.
SDL_SetRenderDrawColor (Window.HandleRenderer, 0, 0, 0, 255);
SDL_RenderClear        (Window.HandleRenderer);

// Render the back buffer texture in the calculated area.
SDL_RenderCopy(
  Window.HandleRenderer, // a handle to the SDL_Renderer
  YourBackBufferTexture, // a handle to the SDL_Texture
  nil,                   // "nil", because we need to render entire back buffer texture
  Window.RectClient      // calculated area that has correct aspect ratio
);

All functions with the Game_SDL_ prefix are direct imports of SDL functions from the DLL. Their names are slightly different from the original ones because I adapted them to the naming convention I use in the entire game project. So if you see, for example, the Game_SDL_WindowSetMouseRect function, it is just a SDL_SetWindowMouseRect function.

This mouse rect setting code is not required for rendering — I forgot to remove it when I pasted the code from the IDE. But if you need the mouse cursor to be unable to leave the area with the rendered frame image (when relative mouse mode is disabled), you can also use the same area to lock the cursor within it, using the SDL_SetWindowMouseRect function. And again, no matter what mode the window is displayed in (small with a system border, fake fullscreen or exclusive video mode), the cursor will be limited to this area.

So create the back buffer texture with this size and never resize it. How it will be stretched on the screen depends entirely on how you calculate the window area in which this texture is to be rendered (in my snippet this area is stored in the Window.RectClient field). I showed you the calculations in the previous post.

No, you don’t have to. The available video modes are only needed if you need to display the game window in an exclusive video mode, with a specific resolution. Create a window in your default size, e.g. twice the size of the back buffer texture (i.e. 800x450). You don’t need anything else.

What the image of the frame in the window will look like is dealt with by the code that calculates the area intended for rendering the frame and the code that renders the contents of the window itself (an example is above, in this post). What I showed will work correctly no matter how you display the window — both in the form of a small window with a system border, in the form of fake fullscreen, and in the exclusive video mode (of any resolution).

1 Like

Thanks for the response. It’s been a few days and I had to go do some research, but I think I have an update - though still don’t understand why Linux and Windows might be handling SDL_WINDOW_FULLSCREEN differently. I’ve implemented a bitmap font that I can use to help identify some performance issues.

I tried the suggestions above and am now scaling me plot surface to be whatever the window size is. I can plot a 400x225 texture-based framebuffer to a 400x225 window:
image

I can also plot the same 400x225 texture-based framebuffer against an 800x450 window:

If I 400x225 buffer against a 400x225 window rect and switch move to SDL_WINDOW_FULLSCREEN_DESKTOP, it finally fills the screen in linux, but with a massive performance hit (I think this means that I am stretching a 400x225 texture over a 1920x1080 desktop resolution):

Ok, so now if I leave the settings above alone but change the renderer flag to SDL_WINDOW_FULLSCREEN, which is what I initially was using at the start of this post when going full screen (@GuildedDoughnut hopefully this addresses your question - thanks for helping me realize there was a difference), it does physically change the desktop resolution, but not down to 400x225, rather I think it is something like 640x480, then plots the 400x225 framebuffer as follows:

The last change I can make to bring it full circle with where the post started is to remove the 400x225 window rect from the RenderCopy() function and just make it NULL so that it covers the full space. Otherwise everything is the same: 400x225 frame buffer, 400x225 window resolution, set to SDL_WINDOW_FULLSCREEN:

If I take exactly that code now and build it in Windows, I get the properly rendered SDL_WINDOW_FULLSCREEN view of 400x225 that I am hoping to see with Linux:


Note that the FPS here is 60 bc it’s in true full screen - possibly the hardware is handling the stretching of the framebuffer to cover the screen properly?

Ok, with all that shown, these might be my remaining questions:

1. Does anyone know why the FPS drops so much when I use SDL_WINDOW_FULLSCREEN_DESKTOP to draw a 400x225 famebuffer as opposed to the 60FPS I get when drawing it in windows mode simply in a window of 400x225? If I could boost the FPS, I think i’d be happy to use this method.

2. This is perhaps a re-phrasing of my original post question: Is it possible that Linux and Windows treat the behavior of SDL_WINDOW_FULLSCREEN differently? If so, why might that happen? Is it a Driver thing? OS thing? I like using SDL_WINDOW_FULLSCREEN because it’s performance-wise fast (or faster than SDL_WINDOW_FULLSCREEN_DESKTOP). I checked a while ago on both Windows and Linux to see what happened when I cout the displaymode resolution (when trying to set 400x225 to full screen) and on both it set the monitor to 640x480 resolution. Windows stretches the resolution to fit the monitor, Linux doesn’t.

Thanks everyone! I appreciate folks time looking at this with me.

Because fake fullscreen is a normal window but without border decoration. The Windows OS have to do more things in such a case, so the overall performace will be lower (sometimes much lower). if you want to have best performance possible, run the game in the exclusive fullscreen mode, so the game process will have exclusive access to the screen and resources (like GPU).

Windows and Linux are different operating systems, so it is possible to handle fullscreen differently. It can be also dependent of the video driver.

Never set up your own resolution. If you need to run the game in exclusive video mode, use any video mode that is supported by your display. You can use the desktop video mode using the SDL_GetDesktopDisplayMode or any other mode obtained by the SDL_GetDisplayMode function (if you need eg. lower resolution).

1 Like

Thanks for your responses. Setting up my own resolution was another thing I was doing wrong! I am starting to understand SDL_WINDOW_FULLSCREEN much better now. I’m also understanding the importance of using available display modes with them, and that every computer/OS will have a different set of available display modes. Also, SDL_WINDOW_FULLSCREEN resolutions need to be chosen from the set of available game modes. This has been most helpful.

For what you’re doing, it should be totally fine to set your video mode to the desktop display resolution, using something like SDL_GetDesktopDisplayMode() like @furious-programming said. Then scale your 400x225 (or whatever) framebuffer to the full screen with SDL_RenderCopy()

400x225 isn’t a standard resolution that’s going to be supported anywhere. Different GPUs, drivers, OSes, and monitors will all handle stuff like that differently.

1 Like

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