--- type: reference title: Display Configuration description: Niri output configuration on blankpad — EDID-identity-keyed desk profiles, hotplug auto-switching, portrait transforms, position pinning, the phantom-mode fallback trap, and GPU notes (AMD Cezanne now; Intel Arc render-node pin is dankware-legacy). tags: - niri - display - monitors - rotation - desk-profiles verified: 2026-10-21 --- # Display Configuration Niri output config on **blankpad** is a **desk-profile system**: the laptop travels between desks, connector names (`DP-2`, `HDMI-A-1`) reshuffle per dock, so panels are configured by **EDID identity** (manufacturer model serial), and *positions* live in per-desk files. A hotplug watcher and a manual picker keep the live layout matched to reality. **Mechanism:** `~/.config/niri/cfg/display.kdl` is a **symlink** to the active profile (`desk-1-home.kdl`, `desk-2-portable.kdl`, …). `config.kdl` includes it with `include optional=true` so a missing link is a warning, not a parse failure. **Probe:** `niri msg outputs` is authoritative (connector, full EDID long name, modes, transform, scale, position, VRR). Per-output changes go live with `niri msg output ` and are temporary — never written to the config file. `wlr-randr` is **not installed** on blankpad (`pacman -Qs wlr` matches nothing, verified 2026-10-21); pages that list it as an installed tool are stale. --- ## The desk-1 rig (home) ``` ┌──────────────┬──────────────────────────────┬──────────┐ │ PM1601 │ Sceptre M34 ultrawide │ eDP-1 │ │ portrait │ 3440x1440 @ 180.000 │ 1920x1080│ │ 1200x1920 │ landscape, scale 1 │ scale 1 │ │ rot 90 │ position 1581,-600 │ 5021,-240│ └──────────────┴──────────────────────────────┴──────────┘ HDMI-A-1 DP-1 (focus-at-startup) eDP-1 (fallback focus) Connector names are today's (`niri msg outputs`, 2026-10-21). They reshuffle with the dock and the cable, which is why the config keys on the EDID long name instead. ``` | Panel | EDID long name | Keyed by | Notes | |-------|----------------|----------|-------| | Sceptre M34 ultrawide | `Sceptre Tech Inc Sceptre M34 Unknown` | identity | `mode "3440x1440@180.000"`, **deliberate** — the panel also offers `3440x1440@239.998`, `144.001` and `120.000`; 180 is driven while the link is degraded (see the law below) — `focus-at-startup` | | PM1601 portrait | *not captured* | connector `HDMI-A-1` | dock-dependent; run `desk.sh capture` when plugged in, then re-key | | eDP-1 laptop panel | `Lenovo Group Limited 0x40A9 Unknown` | identity | scale **1.0** (deliberate; Ghostty grid at 1.25 rejected 2026-10-09), second `focus-at-startup` | VRR: the M34 supports it (its connector today is `DP-1`); **left disabled by decision** (2026-10-09). Add `variable-refresh-rate` per output if that ever changes. `focus-at-startup` on both M34 and eDP-1: niri focuses in config order, so the ultrawide wins when present and the laptop panel is the fallback. ## Desk switching ```mermaid graph LR P[plug / unplug] --> E[niri event-stream
WorkspacesChanged → output-set diff] E --> M[desk-autoswitch.sh
debounced matcher] M --> R{profile requires
subset of connected?} R -->|best match| A[desk.sh apply:
validate - relink - reload - notify] R -->|none| K[keep current + notify] D[Mod+Shift+D] --> C[desk.sh cycle] --> A ``` - **Matcher:** each profile declares `// requires: ` (all must be connected) and `// optional:` comment lines; the most-specific eligible profile wins. - **Apply order is load-config-safe:** `niri validate` the *current* config first, relink `display.kdl`, then `niri msg action load-config-file`, then a noctalia notification names the active desk. A profile that fails validation can never go live. - **Manual override:** `Mod+Shift+D` cycles profiles (wrong auto-pick, or same panels, new layout). - **New desk:** `desk.sh capture` prints connected long names → copy into a new `desk-N-.kdl` (see `desk-2-portable.kdl` skeleton, portable monitor EDID pending first capture). - Unit: `blankpad-desk-autoswitch.service` (systemd user, `ConditionHost=blankpad`). ## The config pattern ```kdl // desk-1-home.kdl — one block per panel, keyed by identity output "Sceptre Tech Inc Sceptre M34 Unknown" { mode "3440x1440@180.000" scale 1 transform "normal" position x=1581 y=-600 focus-at-startup } ``` Four rules learned the hard way: 1. **One output, one block.** Two blocks matching the same output (connector name *and* long name) fight over properties — transform reverts are the classic symptom. Desk profiles are self-contained: a panel's identity, mode, transform, **and** position live in exactly one block per file. `desk-1-home.kdl` still carries legacy connector-keyed `DP-1` / `DP-2` blocks next to the identity block. Their modes were aligned to 180 Hz on 2026-10-21 so a connector-named reconnect cannot drop the panel to 120, but they belong folded into the identity block. 2. **Key by EDID identity, not connector.** Connector names swap between docks; the EDID long name travels with the panel. Serials on these panels read `Unknown`, so the full long name (manufacturer model serial) is the match string — copy it verbatim from `niri msg outputs`. 3. **Positions are absolute, not relational.** A disconnected panel leaves a hole; that is exactly why positions belong to the *desk profile*, not a global file. 4. **Ask only for modes the panel reports, and copy the string exactly.** The M34's `Available modes` (`niri msg outputs`, 2026-10-21) are `3440x1440@239.998`, `3440x1440@180.000`, `144.001`, `120.000`, … with `59.998` marked preferred. niri matches mode strings **verbatim** and does not round, so `239.999` — one digit from the real mode, which is what this file and the config both carried for weeks — is not `239.998`. A mode it cannot find does **not** leave the output dark: niri logs `configured mode … could not be found, falling back to preferred` and quietly drives the panel at its EDID *preferred* mode — **60 Hz**. Nothing looks broken except the refresh rate, and every reconnect repeats the fallback. Check Hz, not just resolution, after any "monitor is acting up". 5. **180 Hz here is a decision, not a ceiling.** The panel offers 239.998 and is driven at 180.000 deliberately: the link trained at `x8 32.0 GT/s` against a max of `x16`, and the display engine is throwing `DMCUB` errors with `EINVAL` page flips after s2idle, so the 240-class mode is not driven until a reboot clears that state. Before changing the mode, re-read `niri msg outputs` **and** the link sysfs; treat "restore 240" as a change to verify on a healthy link, never as the obvious fix. Refs #60. ## "The monitors are broken" — three layers, three signatures A display complaint belongs to one of three layers. Sort it before editing config, because the config is usually innocent — and when it is guilty, the guilt may have been copied from this page. | Layer | Log signature | Reality | Move | |-------|---------------|---------|------| | Config | `configured mode … could not be found, falling back to preferred` | a mode/scale the panel never offered | re-read the mode list with `niri msg outputs` and correct the file; do **not** trust a documented mode | | Link | `disconnecting connector: "DP-1"` followed seconds later by `new connector: DP-1`, recurring | the DP link itself dropping — cable, dock, the monitor's DP-version/DSC setting, or panel power saving | every drop re-runs mode selection, so a config error is re-enacted in public; swap the cable before blaming the compositor | | Driver | `Page flip commit failed … (Invalid argument)` plus kernel `*ERROR* … DMCUB error` | amdgpu's display engine wedged, typically after an s2idle resume | survives until a reboot; niri keeps running while the panel keeps dropping. No config edit clears it | Probes, in this order: ```bash niri msg outputs # connector, EDID long name, real mode list, current mode journalctl --user -b | grep -E 'disconnecting connector|Page flip commit failed|falling back to preferred' journalctl -k -b | grep -iE 'amdgpu.*(ERROR|DMCUB)|PM: suspend exit' ``` Correlate the drop timestamps against `PM: suspend exit` first. A resume a minute before the first drop is the cause; the config file is not. To fix a refresh rate without disturbing the session, use the temporary per-output path — it touches one output, applies no config, reloads nothing: ```bash niri msg output "Sceptre Tech Inc Sceptre M34 Unknown" mode "3440x1440@180.000" ``` It is temporary by design, so correct the profile too and let the next reconnect or `load-config-file` make it permanent. With the display engine already throwing `EINVAL`, prefer the per-output path over a full config reload; on a healthy machine either is fine. ## GPU assignment blankpad is a **single AMD Cezanne iGPU** (`/dev/dri/renderD128`, PCI `07:00.0`). No render-node pin exists in `misc.kdl` — and must not: the blankpad override file deliberately deletes the pin. The `environment { render-node "/dev/dri/renderD129" }` pin found in the *base* `misc.kdl` is **dankware-legacy** (Intel Arc iGPU + discrete GPU hybrid; pin kept the iGPU as display owner and sidesteps the suspend/resume deadlock documented in the sleep-wake investigation). Deploying that base file on blankpad pins a render node that does not exist — this is why blankpad runs the `zz-blankpad` override layer (see [[index|the per-host symlink matrix]]). ## Reload `niri msg action load-config-file` reloads live. Never `SIGHUP` the process — see [[tips]]. ## See Also - [Niri](/tools/niri/index.md) — compositor overview and config layout - [[tips|tips]] — reload path, `niri msg layers/casts`, includes - [[noctalia-shell|noctalia-shell]] — notification shown on desk switch - [[cachyos|cachyos]] — operating system