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

183 lines
9.0 KiB
Markdown

---
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 <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
```mermaid
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
```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** (`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:
```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