Skip to main content

Coordinate Spaces

There are two main coordinate spaces in niri:

Physical space

Pixels of every individual output

Logical space

Shared among all outputs, takes into account the scale of every output
Wayland clients mostly work in the logical space, and it’s the most convenient space to do all the layout in, since it bakes in the output scaling factor.

The Physical Pixel Problem

However, many things need to be sized or positioned at integer physical coordinates.
Things that must align to integer physical pixels:
  • Wayland toplevel buffers (assumed to be placed at an integer physical pixel on an output)
  • Borders and focus rings (should have a width equal to an integer number of physical pixels to stay crisp)
  • SolidColorRenderElement (does not anti-alias lines at fractional pixel positions)

Potential Artifacts

Thus, niri uses fractional logical coordinates for most of its layout. However, one needs to be very careful to keep things aligned to the physical grid to avoid artifacts like:
  • Border width alternating 1 px thicker/thinner
  • Border showing 1 px off from the window at certain positions
  • 1 px gaps around rounded corners
  • Slightly blurry window contents during resizes
  • And so on…

How niri Handles Alignment

The way it’s handled in niri is:
1

Round workspace sizes to physical pixels

All relevant sizes on a workspace are rounded to an integer physical coordinate according to the current output scale.Things that are rounded:
  • Struts
  • Gaps
  • Border widths
  • Working area location
It’s important to understand that they remain fractional numbers in the logical space, but these numbers correspond to an integer number of pixels in the physical space.
2

Rounding formula

The rounding looks something like:
(logical_size * scale).round() / scale
Whenever a workspace moves to an output with a different scale (or the output scale changes), all sizes are re-rounded from their original configured values to align with the new physical space.
3

Don't round offsets initially

The view offset and individual column/tile render offsets are not rounded to physical pixels, but:
4

Round during rendering

tiles_with_render_positions() rounds tile positions to physical pixels as it returns them.Custom shaders like opening, closing and resizing windows, are also careful to keep positions and sizes rounded to the physical pixels.

The Key Insight

Alignment preservation

The idea is that every tile can assume that it is rendered at an integer physical coordinate.Therefore when shifting the position by, say, border width (also rounded to integer physical coordinates), the new position will stay rounded to integer physical coordinates.The same logic works for the rest of the layout thanks to gaps, struts and working area being similarly rounded.
This way, the entire layout is always aligned, as long as it is positioned at an integer physical coordinate (which rounding the tile positions effectively achieves).

Visual Example

Logical coordinates:          Physical coordinates (scale=2):
┌─────────────┐              ┌─────────────────────────┐
│   0.0, 0.0  │              │      0, 0               │
│             │              │                         │
│  Window     │    ───>      │    Window               │
│   at 1.5    │              │     at 3 (rounded)      │
│             │              │                         │
└─────────────┘              └─────────────────────────┘

Border: 0.5 logical          Border: 1 physical
        (rounded from                (crisp!)
         configured value)

Summary

  • Struts
  • Gaps
  • Border widths
  • Working area location
All rounded to integer physical coordinates while stored as fractional logical coordinates.
  • Tile positions (via tiles_with_render_positions())
  • Custom shader positions and sizes
Rounded to physical pixels as they’re used for rendering.
  • View offset
  • Individual column/tile render offsets (until rendering)
These remain fractional but everything still aligns because the rendering step rounds them.

Build docs developers (and LLMs) love