Pixel Density and High-DPI Displays

Learn how to create SDL applications that look great on modern displays across different platforms
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

Modern displays come in various resolutions and pixel densities. In this lesson, we'll learn how to create SDL applications that look great regardless of the display being used. We'll explore DPI awareness, scaling techniques, and cross-platform considerations.

The relationship between pixel dimensions and physical dimensions is usually represented in terms of pixels per inch, or equivalently, dots per inch (DPI)

A higher pixel density (that is, higher DPI) is desirable as it allows graphics to be more detailed. The following image shows a close-up photo of the battery indicator on a phone using a 163DPI screen, compared to the same design rendered on a 326DPI display:

Screenshots comparing two screens of different pixel densities

DPI Awareness

As higher DPI displays became increasingly used, platforms such as Windows had a problem. If content was designed for a lower DPI display, and is about to be rendered on a higher DPI display, that content would be physically smaller.

For example, if we have a 400x400 pixel rectangle rendered on a 100 DPI screen, the physical size of our rectangle will be 4x4 inches. If we render the same rectangle on a 200 DPI screen, it will be half the physical size, at only 2x2 inches.

Content being smaller than intended presents an obvious usability problem. Platforms address this by assuming that programs running on them do not consider that the screen our player is currently using may have a higher DPI than the screen we designed our content for. That is, they assume programs are not DPI-aware.

On Windows, for example, programs that are not DPI-aware are assumed to have been designed for 96 DPI, which was a fairly common pixel density for desktop monitors in the recent past. macOS systems use 72 DPI as their historic baseline with more modern, higher pixel density screens branded as Retina displays.

DPI Scaling and Screen Coordinates

When platforms encounter a DPI-unaware program being run on a high-DPI display, they counteract the physical size reduction by proactively scaling up the DPI-unaware window, and all the content within it.

For example, if the display being used has a pixel density that is 25% higher than the 96DPI that a DPI-unaware program is assumed to have been designed for, that program’s content is scaled up by 25%.

This process is typically called DPI scaling and, on some platforms, users can customise the process. An example from the Windows display settings is below, where we have a 24 inch monitor using a 2560x1440 resolution.

Behind the scenes, Windows calculates this to be around 122 pixels per inch. This is approximately 25% larger than the 96 DPI baseline, so it recommends the user scale DPI-unaware programs to 125% of their designed size:

Screenshots of the display settings in Windows 11

To differentate between the size and position of content before and after this scaling, two different units are used:

  • Screen coordinates are the units used sizes and positions before DPI-scaling is applied
  • Pixels are the units that are used on the player’s screen, after our content is scaled

If no DPI-scaling is applied, screen-coordinate and pixel values are equivalent.

When our program is not DPI-aware, the sizes we use with SDL functions like SDL_CreateWindow() and SDL_GetWindowSize() are in screen coordinates rather than pixels.

For example, if we use SDL_CreateWindow() to create a 800x600 window on a display with 200% DPI scaling, the window will be 1600x1200 pixels on the player’s screen. SDL functions like SDL_GetWindowSize() will continue to use the screen-coordinate values of 800x600.

Enabling DPI-Awareness

Letting our program remain DPI-unaware and having the platform rescale things makes our lives easier, but it denies us the opportunity to take advantage of higher DPI displays.

Scaling up bitmap images, including the frames output by our programs, results in lower quality than rendering a higher resolution image. If a display has double the pixel density, we’d rather use that to render an image with double the resolution, rather than scaling a smaller image to twice its original size.

The first step of this is flagging our program as being DPI-aware. That is, we tell the platform that our program does consider the pixel density of the display it is running on, and reacts appropriately. This means the platform doesn’t need to intervene and resize our content - we’ll take care of it ourselves.

SDL_HINT_WINDOWS_DPI_SCALING

One of the easiest way of flagging our application as being DPI-aware on Windows is through setting the SDL_HINT_WINDOWS_DPI_SCALING hint to "1". This should be done before we initialize the SDL video subsystem:

#include <SDL.h>

int main(int argc, char* argv[]) {
  SDL_SetHint(
    SDL_HINT_WINDOWS_DPI_SCALING, "1");
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Quit();
  return 0;
}

DPI Awareness on Other Platforms

SDL supports a wide range of platforms, but how DPI-awareness is implemented is not standard across all of those platforms. Often, DPI-awareness cannot be configured exclusively by code - it requires supplementary steps specific to that platform.

On most platforms, we need to configure individual windows as being DPI-aware. SDL provides the SDL_WINDOW_ALLOW_HIGHDPI flag to help with this:

SDL_CreateWindow(
  "Window",
  SDL_WINDOWPOS_UNDEFINED,
  SDL_WINDOWPOS_UNDEFINED,
  800, 600,
  SDL_WINDOW_ALLOW_HIGHDPI 
)

On Windows, the SDL_WINDOW_ALLOW_HIGHDPI flag is automatically applied to all windows created after setting the SDL_HINT_WINDOWS_DPI_SCALING hint to "1". We can also flag the window if preferred, which can be useful for cross-platform development:

#ifdef _WIN32
  SDL_SetHint(SDL_HINT_WINDOWS_DPI_SCALING, "1");
#endif

SDL_Window* Window = SDL_CreateWindow(
  "My Game",
  SDL_WINDOWPOS_UNDEFINED,
  SDL_WINDOWPOS_UNDEFINED,
  800, 600,
  SDL_WINDOW_ALLOW_HIGHDPI
);

On macOS, in addition to the SDL_WINDOW_ALLOW_HIGHDPI flag, we need to set the NSHighResolutionCapable value within the Info.plist that describes our application. The macOS documentation is available here.

On Linux, DPI handling depends on the window manager and desktop environment. Most modern environments support high-DPI displays automatically when using the SDL_WINDOW_ALLOW_HIGHDPI flag.

On other platforms, we typically need to refer to the documentation and test our implementation for every platform we want to support.

Working with Pixel Sizes

Now that we’ve flagged our application as being DPI-aware, we’re free to make full use of the additional pixels that high-DPI displays offer. However, we’re now also responsible for ensuring the physical size of our graphics are appropriate across those different pixel densities.

SDL_GetWindowSizeInPixels()

The SDL_GetWindowSizeInPixels() function is typically the easiest way to understand how we need to scale our graphics. It works in much the same way as SDL_GetWindowSize(), accepting three arguments:

  1. The SDL_Window* corresponding to the window we want to retrieve the size of
  2. An int* that will be updated with the pixel width of the window
  3. An int* that will be updated with the pixel height of the window

Here’s an example:

#include <SDL.h>
#include <iostream>

int main(int argc, char* argv[]) {
  SDL_SetHint(
    SDL_HINT_WINDOWS_DPI_SCALING, "1");
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* Window{
    SDL_CreateWindow(
      "Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      800, 600, 0)};
  
  int w, h;
  SDL_GetWindowSizeInPixels(Window, &w, &h);

  std::cout << "Pixel Size: " << w << "x" << h;
  
  SDL_DestroyWindow(Window);
  SDL_Quit();
  return 0;
}

Even though the previous program passed screen coordinates of 800x600 to SDL_CreateWindow(), the SDL_GetWindowSizeInPixels() function reports the window’s pixel size as being 1000x750:

Pixel Size: 1000x750

This is because our window was created on a display that has a 125% DPI scale.

Additionally, because we flagged our program as being DPI-aware through the SDL_HINT_WINDOWS_DPI_SCALING hint, the platform lets us access the real pixel size of our windows. We’re no longer restricted to working with the screen-coordinate abstraction used by SDL_GetWindowSize().

We can still get the screen-coordinate size if we need it and, by comparing it to the pixel size, we can dynamically calculate the DPI scaling factor that is in effect:

#include <SDL.h>
#include <iostream>

int main(int argc, char* argv[]) {
  SDL_SetHint(
    SDL_HINT_WINDOWS_DPI_SCALING, "1");
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* Window{
    SDL_CreateWindow(
      "Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      800, 600, 0
  )};

  int w1, h1;
  SDL_GetWindowSize(Window, &w1, &h1);
  
  int w2, h2;
  SDL_GetWindowSizeInPixels(Window, &w2, &h2);

  std::cout << "Screen Size: " << w1 << "x" << h1
    << ", Pixel Size: " << w2 << "x" << h2
    << ", Scale: " << float(w2) / w1;
    
  SDL_DestroyWindow(Window);
  SDL_Quit();
  return 0;
}
Screen Size: 800x600, Pixel Size: 1000x750, Scale: 1.25

Creating DPI-Aware Graphics

Once we’ve successfully configured our application as being DPI-aware, the platform will no longer scale our content up or down to maintain our design. That responsibility now falls to us.

The simplest way to do this requires us to first calculate the DPI scaling that the platform or user has requested:

#include <SDL.h>
#include <iostream>

float GetDPIScale(SDL_Window* Window) {
  int w1, w2;
  SDL_GetWindowSize(Window, &w1, nullptr);
  SDL_GetWindowSizeInPixels(
    Window, &w2, nullptr);
    
  if (w1 == 0) return 1.0f;
  return float(w2) / w1;
}

int main(int argc, char* argv[]) {
  SDL_SetHint(
    SDL_HINT_WINDOWS_DPI_SCALING, "1");
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* Window{
    SDL_CreateWindow(
      "Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      700, 200, 0)};

  float RenderScale{GetDPIScale(Window)};
  std::cout << "Render Scale: " << RenderScale;
  
  SDL_DestroyWindow(Window);
  SDL_Quit();
  return 0;
}
Render Scale: 1.25

We then provide this scaling factor to any system, function or object that needs to render DPI-aware graphics. That code then applies this scaling factor to its rendering actions.

Below, we define a Render() function that draws a rectangle on a window surface. Our rectangle has a baseline size of 350x200, which we translate to the correct, DPI-aware pixel size by multiplying these dimensions by the scaling factor:

void Render(SDL_Window* Window, float Scale) {
  SDL_Surface* Surface{
    SDL_GetWindowSurface(Window)};
  SDL_Rect Rect{
    0, 0, int(350 * Scale), int(200 * Scale)};
    
  SDL_FillRect(
    Surface, &Rect, SDL_MapRGB(
      Surface->format, 150, 50, 50));
}

A full program that implements these techniques is below:

#include <SDL.h>
#include <iostream>

float GetDPIScale(SDL_Window* Window) {/*...*/}
void Clear(SDL_Window* Window) {/*...*/}
void Render(SDL_Window* Window, float Scale) {/*...*/} int main(int argc, char* argv[]) { SDL_SetHint( SDL_HINT_WINDOWS_DPI_SCALING, "1"); SDL_Init(SDL_INIT_VIDEO); SDL_Window* Window{ SDL_CreateWindow( "Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 700, 200, 0)}; float RenderScale{GetDPIScale(Window)}; SDL_Event E; while (true) { while (SDL_PollEvent(&E)) { if (E.type == SDL_QUIT) { SDL_DestroyWindow(Window); SDL_Quit(); return 0; } } Clear(Window); Render(Window, RenderScale); SDL_UpdateWindowSurface(Window); } return 0; }
Screenshots of the generated program running in Windows

Reacting to DPI Changes

When designing DPI-aware graphics, we should be mindful that the DPI scaling factor can change during the lifecycle of our program. This can happen when the user updates their display settings, or moves our window to a display with a different pixel density.

Our previous program doesn’t consider this, so the visual design changes when the DPI changes:

Screenshots of the generated program running in Windows

To ensure our program behaves correctly, we need to find a way to detect and react to these DPI changes. The window being moved is a situation where we will want to recalculate the scaling factor, as it will take care of scenarios where the user is moving our window between displays with different DPIs.

Other scenarios where DPI changes, and how to detect those scenarios, varies from platform to platform and requires testing. For example, when using the SDL_HINT_WINDOWS_DPI_SCALING hint on Windows, SDL will automatically resize our window when the DPI scale changes.

We can detect window resizes and movements through our event loop in the usual way. Below, we recalculate our scaling factor when these events are detected:

#include <SDL.h>
#include <iostream>

float GetDPIScale(SDL_Window* Window) {/*...*/}
void Clear(SDL_Window* Window) {/*...*/}
void Render(SDL_Window* Window, float Scale) {/*...*/} int main(int argc, char* argv[]) { SDL_SetHint( SDL_HINT_WINDOWS_DPI_SCALING, "1"); SDL_Init(SDL_INIT_VIDEO); SDL_Window* Window{ SDL_CreateWindow( "Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 700, 200, 0)}; float RenderScale{GetDPIScale(Window)}; SDL_Event E; while (true) { while (SDL_PollEvent(&E)) { if (E.type == SDL_QUIT) { SDL_DestroyWindow(Window); SDL_Quit(); return 0; } if (E.type == SDL_WINDOWEVENT) { if (E.window.event == SDL_WINDOWEVENT_MOVED || E.window.event == SDL_WINDOWEVENT_RESIZED) { RenderScale = GetDPIScale(Window); } } } Clear(Window); Render(Window, RenderScale); SDL_UpdateWindowSurface(Window); } return 0; }

Summary

In this lesson, we've explored how to create SDL applications that work seamlessly across different display resolutions and pixel densities. We've learned about DPI awareness, scaling techniques, and how to handle resolution changes dynamically. Key takeaways:

  • DPI awareness is important for modern applications to look good on high-resolution displays
  • The SDL_HINT_WINDOWS_DPI_SCALING hint enables DPI awareness on Windows
  • Use SDL_GetWindowSizeInPixels() to get actual pixel dimensions
  • Scale graphics based on the current DPI scaling factor
  • Monitor window events to handle DPI changes during runtime
  • Consider platform-specific requirements for proper DPI handling

Was this lesson useful?

Next Lesson

Handling Mouse Scrolling

Learn how to detect and handle mouse scroll wheel events in SDL2, including vertical and horizontal scrolling, as well as scroll wheel button events.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Monitors and Display Modes
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 71 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Handling Mouse Scrolling

Learn how to detect and handle mouse scroll wheel events in SDL2, including vertical and horizontal scrolling, as well as scroll wheel button events.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved