Files
command-center/omp-docs/tools/niri/displays.md
T
Frank 62a70caebb docs(okf): 53 Obsidian wikilinks become OKF links
`[[page|Label]]` is Obsidian syntax, not OKF. `kb-lint` resolves it Obsidian-style — by
filename, anywhere in the bundle — so it has never tripped the link gate, while every other
consumer of the bundle (an OKF renderer, a Gitea wiki export, `grep` for reachability) sees
bracketed text where a link should be. That is how two generated pages ended up with no
inbound link at all and nobody noticed: the my-first link census of the bundle reported them
as orphans because their only inbound edges were wikilinks.

Mechanical conversion, 52 links across 17 pages, each resolved against the same page set
`kb-lint` uses: an explicit `|Label` is kept verbatim, an unlabelled link takes the target's
frontmatter `title`. Targets are emitted bundle-absolute (`/tools/niri/tips.md`), the form
the spec recommends and the form 99 existing links in this bundle already use.

Two links were `[[index]]`, which resolves by filename to 21 pages — the ambiguity that
filename-resolution invites. Both meant their own directory's index, so both are now written
out in full: `/tools/gitea/index.md`, `/tools/niri/index.md`.

The linter still tolerates wikilinks; whether it should reject them outright is an open
question on #64, because that would replace the two tests pinning today's tolerance.

Refs #64
2026-10-21 02:11:56 -07:00

195 lines
10 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-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<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, 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 [the per-host symlink
matrix](/tools/niri/index.md)).
## Reload
`niri msg action load-config-file` reloads live. Never `SIGHUP` the process
— see [Niri Tips and Gotchas](/tools/niri/tips.md).
## See Also
- [Niri](/tools/niri/index.md) — compositor overview and config layout
- [tips](/tools/niri/tips.md) — reload path, `niri msg layers/casts`, includes
- [noctalia-shell](/tools/noctalia-shell.md) — notification shown on desk switch
- [cachyos](/tools/cachyos.md) — operating system