Pixel Density and High-DPI Displays
Learn how to create SDL3 applications that look great on modern displays across different platforms
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:

This lesson continues to use the Window class we created previously:
src/Window.h
#pragma once
#include <SDL3/SDL.h>
class Window {
public:
Window() {
SDLWindow = SDL_CreateWindow(
"Window", 700, 200,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE
);
}
SDL_Window* GetRaw() const {
return SDLWindow;
}
void Render() {
const auto* Fmt = SDL_GetPixelFormatDetails(
GetSurface()->format
);
SDL_FillSurfaceRect(
GetSurface(),
nullptr,
SDL_MapRGB(Fmt, nullptr, 50, 50, 50)
);
}
void Update() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() const {
return SDL_GetWindowSurface(SDLWindow);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
SDL_Window* SDLWindow{nullptr};
};DPI Awareness
As higher DPI displays became increasingly common, platforms like Windows and macOS faced a problem: older applications designed for low-DPI screens appeared physically smaller on high-DPI displays.
To solve this, operating systems introduced DPI scaling. They assume that older applications are not "DPI-aware" and automatically scale up their windows and content to maintain a consistent physical size. For example, on a display with 200% scaling, a DPI-unaware 800x600 window is rendered by the OS into a 1600x1200 pixel area.
This automatic scaling makes old apps usable, but it often results in blurry or pixelated graphics because it's just stretching a low-resolution image.
To differentiate between the size before and after this scaling, two units are used:
- Screen coordinates (or points) are the logical units an application works with.
- Pixels are the physical dots on the display.
A major improvement in SDL3 is that it is DPI-aware by default. Unlike SDL2, you no longer need to set special hints like SDL_HINT_WINDOWS_DPI_SCALING to opt-in. SDL3 applications automatically understand and work with the native pixel density of the display.
To ensure your window can take full advantage of high-DPI capabilities, you can use the SDL_WINDOW_HIGH_PIXEL_DENSITY flag when creating it. On many platforms, this is now the default behavior, but explicitly setting it ensures cross-platform consistency.
SDL_Window* Window = SDL_CreateWindow(
"My Game",
800, 600,
SDL_WINDOW_HIGH_PIXEL_DENSITY
);Working with Pixel Sizes and Scaling
To take advantage of higher quality screens, we first need to understand what pixel density we're working with. SDL3 provides new tools to make this straightforward.
Getting the Display Scale
The most direct way to handle DPI is to get the display's scaling factor. The SDL_GetWindowDisplayScale() function returns this as a float.
- A value of
1.0fmeans no scaling (e.g., a standard 96 DPI display on Windows). - A value of
2.0fmeans the display has double the pixel density (a "Retina" display). - Other values like
1.25for1.5frepresent intermediate scaling levels.
src/main.cpp
#include <SDL3/SDL.h>
#include <iostream>
#include "Window.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
float RenderScale{SDL_GetWindowDisplayScale(
GameWindow.GetRaw()
)};
std::cout << "Render Scale: " << RenderScale;
SDL_Event E;
bool isRunning{true};
while (isRunning) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_EVENT_QUIT) {
isRunning = false;
}
}
GameWindow.Render();
GameWindow.Update();
}
SDL_Quit();
return 0;
}Render Scale: 1.25You can use this scale factor to convert between logical screen coordinates and physical pixels:
Retrieving Pixel Size Directly
If you need the exact pixel dimensions of the window's drawable area (the surface), you can use SDL_GetWindowSizeInPixels(). This is useful when you need to work directly with pixel buffers or configure graphics APIs like OpenGL or Vulkan.
Depending on the platform and your settings, the value returned from SDL_GetWindowSizeInPixels() may be different from that returned by SDL_GetWindowSize().
This is just based on what units that platform uses to represent window sizes. On Windows, for example, these values will be the same, but they will likely be different on macOS:
src/main.cpp
#include <SDL3/SDL.h>
#include <iostream>
#include "Window.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
int w1, h1;
SDL_GetWindowSize(
GameWindow.GetRaw(), &w1, &h1
);
int w2, h2;
SDL_GetWindowSizeInPixels(
GameWindow.GetRaw(), &w2, &h2
);
std::cout << "Screen Size: " << w1 << "x" << h1
<< ", Pixel Size: " << w2 << "x" << h2;
SDL_Event E;
bool isRunning{true};
while (isRunning) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_EVENT_QUIT) {
isRunning = false;
}
}
GameWindow.Render();
GameWindow.Update();
}
SDL_Quit();
return 0;
}Screen Size: 700x200, Pixel Size: 700x200Window Display Scale vs Display Content Scale
We've introduced SDL_GetWindowDisplayScale() as the primary way of working with varying pixel-densities, but you might notice another, similar-sounding function: SDL_GetDisplayContentScale(). You should almost always use SDL_GetWindowDisplayScale(), but this section briefly explains the differences.
The existence of two similar functions is a result of SDL3 abstracting away major differences in how operating systems handle high-DPI scaling.
Windows, Android, and X11 (Linux)
On these platforms, window coordinates are typically treated as physical pixels. A call to SDL_CreateWindow() for an 800x600 window creates an 800x600 pixel window.
The SDL_GetDisplayContentScale() will be greater than 1.0 (e.g., 1.5 for 150% scaling). On these systems, SDL_GetWindowDisplayScale() will usually return the same value as SDL_GetDisplayContentScale().
macOS, iOS, and Wayland (Linux)
On these platforms, window coordinates are logical "points". An 800x600 window is 800x600 points. If the window has the SDL_WINDOW_HIGH_PIXEL_DENSITY flag on a high-DPI (Retina) display, the operating system gives it a backing framebuffer with more pixels (e.g., 1600x1200).
On these systems, SDL_GetDisplayContentScale() is usually 1.0, and the scaling is communicated through the window's pixel density - SDL_GetWindowPixelDensity().
Comparing Functions
The recommended SDL_GetWindowDisplayScale() function provides the correct, unified scaling factor regardless of the underlying platform's model. It correctly combines the pixel density and the content scale to give you the single number you care most about: "By what factor do I multiply my logical sizes to get the correct pixel sizes for rendering?"
The official SDL documentation provides more information on these functions, as well as a numeric example for a 4K (3840x2160) monitor with 200% scaling. The table below illustrates how SDL3 unifies the different platform approaches:
| Value | macOS (High-DPI) | Windows (200% Scale) |
|---|---|---|
SDL_GetWindowSize() | 1920x1080 | 3840x2160 |
SDL_GetWindowSizeInPixels() | 3840x2160 | 3840x2160 |
SDL_GetDisplayContentScale() | 1.0f | 2.0f |
SDL_GetWindowPixelDensity() | 2.0f | 1.0f |
SDL_GetWindowDisplayScale() | 2.0f | 2.0f |
Notice how, despite the underlying differences, SDL_GetWindowDisplayScale() returns the correct 2.0f multiplier on both platforms.
This makes it the most reliable choice for cross-platform code, although we should of course test our program on all platforms we're releasing on.
Creating DPI-Aware Graphics
Once we know what scaling factor is being applied, any component that can benefit from high-DPI displays can react to that value. A component that loads images, for example, might select to use higher-resolution images if the application is running on a high-DPI display.
To set this up, we simply make the scaling factor available to any component in our application that might want it. The following Render() function receives not just the SDL_Surface to render on, but also the DPI scaling factor that is currently in effect:
Files

Reacting to DPI Changes
The DPI scaling factor is not constant. It can change during your application's lifetime, most commonly when the user drags the window from one display to another with a different pixel density.
If we don't react to these changes then, depending on the platform, our graphics may no longer be rendering in the way we intended:

To create a seamless experience, our application needs to detect and respond to these changes. SDL3 provides two main events for this:
SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: This event is sent when the scale factor of the display the window is on changes. That is, after this event, we'd expectSDL_GetWindowDisplayScale()to return a different value.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: This fires whenever the backing pixel buffer of the window changes size. This happens as a result of regular window resizing, but it can also happen as a result of DPI changes - ie, whereSDL_GetWindowPixelDensity()would return a new value. After a window pixel size change event, we'd expectSDL_GetWindowSizeInPixels()to calculate different values, but not necessarilySDL_GetWindowSize().
We can listen for one or both of these events in our main loop and have our components react as appropriate based on the type of content they're rendering.
Below, we recalculate our RenderScale whenever either changes, which influences how our Rectangle is scaled:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"
#include "Rectangle.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Rectangle Rect;
float RenderScale{SDL_GetWindowDisplayScale(
GameWindow.GetRaw()
)};
SDL_Event E;
bool isRunning{true};
while (isRunning) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_EVENT_QUIT) {
isRunning = false;
}
if (E.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED ||
E.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED) {
RenderScale = SDL_GetWindowDisplayScale(
GameWindow.GetRaw()
);
std::cout << "DPI Scale changed to: "
<< RenderScale << '\n';
}
}
GameWindow.Render();
Rect.Render(GameWindow.GetSurface(), RenderScale);
GameWindow.Update();
}
SDL_Quit();
return 0;
}Summary
In this lesson, we've explored how to create SDL3 applications that work 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 enabled by default in SDL3, but using the
SDL_WINDOW_HIGH_PIXEL_DENSITYflag is good practice for cross-platform consistency. - Use
SDL_GetWindowDisplayScale()to get the current display's scaling factor. SDL_GetWindowSizeInPixels()provides the exact pixel dimensions of the window's drawable surface.- Scale graphics at render time based on the current DPI scaling factor to maintain physical size.
- Monitor window events like
SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGEDandSDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGEDto handle DPI changes dynamically, such as when a window is moved between monitors.