displays.md still described desk-2-portable as an EDID-pending skeleton and left the PM1601 identity unnamed. record the second desk's rig, its position math, and the law that a rig with no eligible profile runs the previous desk's values rather than "unchanged" — the failure that left the portable panels on 3440x1440@180 fallbacks for weeks.
13 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). |
|
2026-10-30 |
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-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 | captured 2026-10-30 — the two panels are PNP(XYM) PM1601 0000000000000 and PNP(XYM) PM1601 demoset-1 |
connector HDMI-A-1 in this profile; identity on the portable desk |
dock-dependent; the identity key is known now, so re-key the block when the panel next rotates through a dock |
| 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.
The desk-2 rig (portable)
Laptop plus two 16" PM1601 panels in portrait, side by side as one 2400x1920
band. Profile: cfg/desk-2-portable.kdl (display.kdl symlinks to it).
┌──────────────┬──────────┬──────────┐
│ │ PM1601 │ PM1601 │ eDP-1 1920x1080 normal x=0,y=420
│ │ rot 90 │ rot 90 │ DP-2 1200x1920 rot 90 x=1920,y=0
│ eDP-1 │ 1200x1920│ 1200x1920│ HDMI-A-1 1200x1920 rot 90 x=3120,y=0
│ 1920x1080 │ (DP-2) │(HDMI-A-1)│
│ │ │ │ The laptop is pinned 420 px down so its
└──────────────┴──────────┴──────────┘ centre (960) meets the band's centre:
x=0,y=420 x=1920,0 x=3120,0 no dead corner when the cursor crosses.
All three blocks are identity-keyed and pin their mode string verbatim. The
requires: list names all three panels, so the matcher only claims this profile
when the whole rig is present — unplug one 16" panel and the watcher leaves the
current layout alone rather than shuffling the survivors.
The stale-profile trap this fixed (2026-10-30). Before this profile existed,
the portable rig ran with display.kdl → desk-1-home.kdl, whose identity
blocks belong to the Sceptre ultrawide that is not connected and whose
connector-keyed fallbacks carried mode "3440x1440@180.000". niri found no such
mode on a 1920x1200 panel, logged the fallback, and drove it at preferred — so
the panels looked fine while every position and refresh value in force had been
written for a different desk. desk.sh match reported "no desk profile matches
connected outputs" and blankpad-desk-autoswitch.service therefore died at its
initial match, leaving the laptop pinned to the wrong profile indefinitely.
Law: a desk with no matching profile is not "unchanged", it is silently
misconfigured — desk.sh match saying "no match" is a defect to fix before
anything else on that desk behaves strangely.
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 validatethe current config first, relinkdisplay.kdl, thenniri 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+Dcycles profiles (wrong auto-pick, or same panels, new layout). - New desk:
desk.sh captureprints connected long names → copy into a newdesk-N-<name>.kdlwith one identity block per panel (template: the desk-2 rig). - A dead watcher is a signal, not a crash to ignore: the unit exits at
startup when
desk.sh matchfinds no eligible profile, so "no match" on a desk that clearly has panels means a missing profile, not an idle daemon. Checksystemctl --user is-active blankpad-desk-autoswitchafter touching desk profiles. - 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:
- 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.kdlstill carries legacy connector-keyedDP-1/DP-2blocks 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. - 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 fromniri msg outputs. - 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.
- Ask only for modes the panel reports, and copy the string exactly. The M34's
Available modes(niri msg outputs, 2026-10-21) are3440x1440@239.998,3440x1440@180.000,144.001,120.000, … with59.998marked preferred. niri matches mode strings verbatim and does not round, so239.999— one digit from the real mode, which is what this file and the config both carried for weeks — is not239.998. A mode it cannot find does not leave the output dark: niri logsconfigured mode … could not be found, falling back to preferredand 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". - 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/sagainst a max ofx16, and the display engine is throwingDMCUBerrors withEINVALpage flips after s2idle, so the 240-class mode is not driven until a reboot clears that state. Before changing the mode, re-readniri msg outputsand 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:
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 the per-host symlink
matrix).
Reload
niri msg action load-config-file reloads live. Never SIGHUP the process
— see Niri Tips and Gotchas.
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