Skip to main content

Display Scaling somewm-only

This page explains how display scaling works in SomeWM and Wayland compositors.

Physical vs Logical Pixels

Modern high-resolution displays pack more pixels into the same physical space:

DisplayResolutionSizePixel Density
Standard 1080p1920x108024"~92 PPI
4K3840x216027"~163 PPI
4K HiDPI3840x216015" laptop~294 PPI

Without scaling, a 4K display shows everything at half the physical size of a 1080p display. Text becomes unreadable.

Scaling creates a distinction between two coordinate systems:

  • Physical pixels - Actual hardware pixels on your display
  • Logical pixels - The coordinate system applications use

At scale 2.0, one logical pixel equals four physical pixels (2x2). The UI appears the same physical size but renders at higher detail.

Scale 1.0:  1 logical pixel = 1 physical pixel
Scale 1.5: 1 logical pixel = 2.25 physical pixels (1.5 x 1.5)
Scale 2.0: 1 logical pixel = 4 physical pixels (2 x 2)

Compositor-Side Scaling

Wayland handles scaling fundamentally differently from X11.

X11 Approach

On X11, scaling was an afterthought:

  1. Apps render at whatever size they want
  2. The X server doesn't know about scaling
  3. Tools like xrandr --scale zoom the entire framebuffer
  4. Result: blurry rendering because scaling happens after the fact

Wayland Approach

Wayland builds scaling into the protocol:

  1. The compositor tells apps what scale factor to use
  2. Apps render at the appropriate resolution for that scale
  3. The compositor places buffers at the correct position
  4. Result: crisp rendering because apps know the target resolution
X11:     App renders → X Server displays → Zoom (blurry)
Wayland: Compositor tells scale → App renders at scale → Display (crisp)

Fractional Scaling

Integer scaling (1x, 2x) is straightforward. Fractional scaling (1.25x, 1.5x) is more complex.

The Problem

At scale 1.5, a 10-pixel line should be 15 physical pixels. But if a line starts at pixel 0.5, it spans pixels 0-14.5. Where does pixel 14.5 go?

The Protocol: wp_fractional_scale_v1

Wayland's wp_fractional_scale_v1 protocol solves this:

  1. Compositor advertises the exact fractional scale (e.g., 1.5)
  2. App renders at a larger size (e.g., 3x the logical size)
  3. Compositor downscales to the display (3x → 1.5x = 2x physical)
  4. Careful math ensures pixel-perfect alignment

Apps that support this protocol render crisply at any fractional scale. Apps that don't fall back to integer scaling with compositor downscaling.

App Support

App TypeFractional SupportNotes
GTK4FullNative protocol support
Qt6FullNative protocol support
GTK3Integer onlyUse GDK_SCALE=2 and compositor downscale
ElectronVariesSet ELECTRON_OZONE_PLATFORM_HINT=wayland
XWaylandInteger onlyX11 apps through compatibility layer

DPI vs Scale

These terms are related but distinct:

DPI (Dots Per Inch) - A measure of pixel density based on physical display size:

-- DPI calculation
local diagonal_pixels = math.sqrt(width^2 + height^2)
local diagonal_inches = math.sqrt(mm_width^2 + mm_height^2) / 25.4
local dpi = diagonal_pixels / diagonal_inches

Scale - A multiplier that tells apps how to render:

-- Logical dimensions
logical_width = physical_width / scale
logical_height = physical_height / scale

When to Use Each

ScenarioUse
Sizing fonts/widgetsbeautiful.xresources.apply_dpi(value)
Configuring display outputscreen.scale = 1.5
Per-app workaroundsEnvironment variables like GDK_SCALE

Auto-DPI

SomeWM can automatically calculate DPI from physical monitor dimensions:

awful.screen.set_auto_dpi_enabled(true)

This reads mm_width and mm_height from the monitor's EDID data and computes appropriate DPI values.

Mixed-DPI Setups

Multiple monitors with different pixel densities are challenging:

┌─────────────────┐  ┌──────────┐
│ 4K @ 1.5x │ │ 1080p │
│ 2560x1440 │ │ 1920x1080│
│ logical │ │ @ 1.0x │
└─────────────────┘ └──────────┘

The Challenge

  • Windows moving between screens need to resize
  • Widgets need to adapt their DPI
  • Coordinates at screen boundaries must align

SomeWM's Approach

Each screen has its own scale property:

awful.screen.connect_for_each_screen(function(s)
if s.geometry.width > 3000 then
s.scale = 1.5
else
s.scale = 1.0
end
end)

The compositor handles coordinate translation at screen boundaries. When a client moves between screens, it receives the new scale factor and re-renders.

How Widgets Handle Scale

SomeWM widgets use DPI-aware sizing:

local dpi = require("beautiful.xresources").apply_dpi

-- This scales with display DPI
theme.wibar_height = dpi(24)
theme.useless_gap = dpi(4)

When screen.scale changes, SomeWM:

  1. Emits property::scale signal
  2. Invalidates drawable surfaces
  3. Widgets re-render at the new scale
  4. dpi() function returns scaled values

Troubleshooting

Blurry Text on Fractional Scales

Some apps don't support wp_fractional_scale_v1. Options:

  1. Use integer scaling (1.0, 2.0) for those apps
  2. Force the app to use a higher scale, let compositor downscale
  3. Switch to a Wayland-native alternative

XWayland Apps Look Wrong

X11 apps through XWayland only support integer scaling. The compositor scales their buffers, which can cause blur at fractional scales.

Workarounds:

# Force GTK3 app to render at 2x, compositor downscales
GDK_SCALE=2 my-gtk3-app

# Force Qt app scaling
QT_SCALE_FACTOR=1.5 my-qt-app

Widgets Don't Update After Scale Change

Ensure you're using dpi() for all size values in your theme:

-- Wrong: fixed pixels, won't scale
theme.useless_gap = 4

-- Right: DPI-aware, scales automatically
theme.useless_gap = dpi(4)

See Also