Files
command-center/omp-docs/tools/niri/displays.md
T
Frank 23372e96f5 fix(niri): pin the ultrawide to the 180 Hz mode its EDID actually offers
desk-1-home.kdl asked the Sceptre M34 for 3440x1440@239.999, a mode the panel
has never offered. niri does not reject an unavailable mode: it logs "configured
mode ... could not be found, falling back to preferred" and drives the EDID
preferred mode - 60 Hz - on every connector event, which is why the monitor
looked like it was dying rather than misconfigured. Read live with
`niri msg outputs`, the mode list tops out at 180.

Also destroys the copies of the phantom number: displays.md carried 239.999 in
its diagram, its panel table and its copy-paste pattern block, and its rule 4
claimed an impossible mode leaves the output dark. Rule 4 now states the real
failure mode, a new section separates config / DP-link / amdgpu-driver faults by
log signature, and wlr-randr is corrected to not-installed (three pages claimed
it; `pacman -Qs wlr` matches nothing).

Applied live with `niri msg output <panel> mode <mode>` - the temporary
per-output path, so no config reload and no logged-out session - verified at
3440x1440 @ 180.000 Hz with the compositor PID unchanged.

Refs #58
2026-10-21 00:05:17 -07:00

9.0 KiB

type, title, description, tags, verified
type title description tags verified
reference Display Configuration 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).
niri
display
monitors
rotation
desk-profiles
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 <name> <mode|scale|transform|position|off|on> 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-2 (focus-at-startup)     eDP-1 (fallback focus)
Panel EDID long name Keyed by Notes
Sceptre M34 ultrawide Sceptre Tech Inc Sceptre M34 Unknown identity mode "3440x1440@180.000" — the EDID maximum; no 240 Hz mode exists — 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: DP-2 supports it; 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

graph LR
    P[plug / unplug] --> E[niri event-stream<br/>WorkspacesChanged → output-set diff]
    E --> M[desk-autoswitch.sh<br/>debounced matcher]
    M --> R{profile requires<br/>subset of connected?}
    R -->|best match| A[desk.sh apply:<br/>validate - relink - reload - notify]
    R -->|none| K[keep current + notify]
    D[Mod+Shift+D] --> C[desk.sh cycle] --> A
  • Matcher: each profile declares // requires: <long name> (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-<name>.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

// 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 (niri msg outputs → Mode: list). A mode the panel never offered 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 on the M34. Nothing looks broken except the refresh rate, and every reconnect repeats the fallback. Check Hz, not just resolution, after any "monitor is acting up".

"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:

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:

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 — compositor overview and config layout
  • tips — reload path, niri msg layers/casts, includes
  • noctalia-shell — notification shown on desk switch
  • cachyos — operating system