`[[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
195 lines
10 KiB
Markdown
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
|