Skip to content

Sleep external monitors over DDC/CI instead of DPMS behind a ddc-blank toggle - #16457

Open
kstaikov wants to merge 4 commits into
omacom:quattrofrom
kstaikov:ddc-blank
Open

kstaikov wants to merge 4 commits into
omacom:quattrofrom
kstaikov:ddc-blank

Conversation

@kstaikov

@kstaikov kstaikov commented Oct 24, 2026 •

Copy link
Copy Markdown

Refs #13812, #8580, #13129

Problem

Some HDMI 2.1 monitors drop hotplug-detect when the output is turned off. Behind the lock screen that becomes the loop in #13812: the kernel removes the output, the monitor re-asserts hotplug a few seconds later, Hyprland re-adds it lit, the lock plugin re-arms its blank timer, and the monitor turns off again, every ~40 s for as long as the session is locked. On a current AMD kernel the same monitors then need a long HDMI 2.1 FRL link training after a real standby, which amdgpu 7.2 abandons after one attempt, so wake takes 40–50 s of key-mashing (AMD issue 5709). The kernel-side debounce in #13129 stops the loop but makes the slow wake the normal case, and it deadlocked amdgpu once on my machine (details on that PR).

Change

The display never needs to go off for the monitor to sleep. With omarchy toggle ddc-blank on and only external monitors enabled, omarchy-brightness-display off|on writes the monitor's VCP power mode (D6 = 04 to sleep, 01 to wake) through omarchy-brightness-display-ddc instead of dispatching DPMS. The GPU keeps the link trained, so nothing disconnects, nothing reconnects, and wake is instant. The lock plugin does not change: it already blanks and wakes through that script.

  • omarchy-brightness-display-ddc <monitor> off|on writes D6 on the monitor's cached bus. The write is unverified (--noverify), because a monitor that is off no longer answers the read-back and a verified write reports failure for a command it applied.
  • omarchy-brightness-display off sleeps every enabled monitor in parallel and falls back to DPMS if any of them refused, so the screen always goes dark. Each monitor that took the sleep is marked asleep under $XDG_RUNTIME_DIR/omarchy-ddc-blank/. on wakes exactly those monitors, whatever the monitor set has become since (a laptop lid reopened before the wake, say), and wakes each once however many wake requests the lock view sends (one per key press and pointer move; a single unlock produced 27 omarchy-system-wake runs in my journal). A monitor that ignores the wake gets a DPMS cycle, which is the modeset it would have had before.
  • Blank and wake share a lock. The lock screen starts them without waiting for one another, and a blank that is slow to fail over to DPMS must not turn the displays off after a newer wake has lit them. The lock waits without a timeout: nothing holds it longer than its DDC writes, which the I2C layer bounds, and giving up on a timeout would mean acting without the ordering the lock is for. The lock and the asleep markers live in $XDG_RUNTIME_DIR only; without a private runtime directory the toggle is ignored and DPMS blanks as before.
  • The DDC path applies only when every enabled monitor is external (and, on a platform whose displays.conf says so, has a ddc node). A laptop with its lid open keeps DPMS for everything, since the panel has no DDC and mixing the two would leave one display lit.
  • omarchy-toggle-ddc-blank and two menu rows under Toggle, with English catalog entries. Off by default; nothing changes for anyone who does not turn it on.

Testing

  • test/shell.d/brightness-display-test.sh gains ten cases against a mock compositor that remembers its DPMS state: DPMS stays the default; the toggle writes setvcp D6 04 without a DPMS dispatch; without a runtime directory the toggle is ignored; a built-in panel keeps DPMS; a monitor slept over DDC is woken over DDC even once a built-in panel is back; a burst of wakes writes once and leaves lit displays alone; a refused DDC sleep falls back to DPMS and is then woken with DPMS; an ignored DDC wake gets a DPMS cycle; a wake arriving during a slow, failing blank still ends with the display lit. i18n-test.sh and ./test/cli pass.
  • On the machine that motivated this (Ryzen 9 9950X / Radeon 610M, MSI B850M Mortar with HDMI as its only output, Dell U3224KB at 6144x3456@60): 13 hours of use with ten lock cycles including one of 7 h 20 min. Zero Disabling output and zero Connector … disconnected lines in the Hyprland log, 6K@60 intact after every wake, picture back within a second of the first key press. The monitor draws 0.5 W asleep, the same as in DPMS standby.

Caveats

DDC/CI has to be enabled in the monitor's OSD (it is by default on every Dell I have seen). A monitor without DDC/CI simply never takes the command and gets DPMS as before. ddcutil is already a dependency of omarchy-brightness-display-ddc.

🤖 Generated with Claude Code

@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with the previous ordering-test race fixed.

Summary

Adds an off-by-default ddc-blank toggle that sleeps external monitors over DDC/CI while keeping the video link up.

  • External monitors can sleep over DDC/CI when the toggle is on.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Blank request] --> B[Acquire shared lock]
  B --> C{DDC sleep allowed?}
  C -->|Yes| D[Sleep external monitors and save markers]
  C -->|No| E[Blank with DPMS]
  D -->|Any write fails| E
  D --> F[Release lock]
  E --> F
  G[Wake request] --> H[Acquire shared lock]
  H --> I[Wake monitors listed in markers]
  I --> J[Use DPMS fallback if needed]
  J --> K[Release lock]
Loading

Reviews (5) · Last reviewed commit: "Start the wake in the ordering test only..." · Reviewed by Greptile

Comment thread bin/omarchy-brightness-display Resolved
Comment thread bin/omarchy-brightness-display Resolved
Comment thread test/shell.d/brightness-display-test.sh Resolved
Comment thread bin/omarchy-brightness-display Resolved

@chessing234 chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flock -w 10 || true keeps going when the lock is not held. blank and wake are supposed to share that lock so a slow blank cannot turn the displays off after a newer wake, and the timeout path skips it.

@kstaikov

Copy link
Copy Markdown
Author

Thanks, that was a hole: the timeout path went ahead without the lock, which is the exact race the lock is there for. The lock is now taken without a timeout, so a wake always runs after the blank in flight and a blank after the wake in flight. Nothing holds it longer than its DDC writes, which the I2C layer bounds, and both processes are started asynchronously by the lock plugin, so a wait never blocks the shell. I checked this on the hardware by holding the lock from another process for three seconds: the wake waited the three seconds, then lit the monitor. Also moved the lock and the markers out of the /tmp fallback per Greptile's finding. Fixed in 9b2e1d3.

(Prepared with Claude's help; reviewed and posted by me.)

kstaikov and others added 3 commits November 2, 2026 10:46
…k toggle

Some HDMI 2.1 monitors drop hotplug when the output is turned off. Behind
the lock screen that removes and re-adds the display every ~40 s, each
time relighting it, and after a real standby the link needs a long FRL
training to come back (40-50 s on a Dell U3224KB with amdgpu 7.2, which
gives up after one attempt). Keeping the link up avoids both.

With `omarchy toggle ddc-blank` on and only external monitors enabled,
`omarchy-brightness-display off|on` now writes the monitor's VCP power
mode (D6 = 04/01) through omarchy-brightness-display-ddc instead of
dispatching DPMS. The write is unverified, since a monitor that is off no
longer answers the read-back, and wakes are throttled to one a second
because the lock screen requests one on every key press and pointer
move. A built-in panel, a monitor that refuses the command, or a monitor
that will not wake falls back to DPMS, so nothing is left lit or dark.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…inst wake

`on` picked the monitors to wake from the current monitor set, so a laptop
whose lid reopened between the blank and the wake found a built-in panel,
sent no DDC wake, and left the external monitors dark: DPMS still read them
as on. Each monitor that takes the sleep is now marked asleep under
$XDG_RUNTIME_DIR/omarchy-ddc-blank/ and `on` wakes exactly those, which also
makes a burst of wake requests write once per monitor without a clock-based
throttle, and removes the test that depended on two runs sharing a second.

The lock screen starts blank and wake without waiting for one another, so a
blank slow to fail over to DPMS could turn the displays off after a newer
wake had lit them. Blank and wake now share a lock; the wake waits and has
the last word.

The test's mock compositor remembers its DPMS state, so a wake after a DPMS
blank has something to turn back on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…out a timeout

The lock and the asleep markers were falling back to a shared /tmp path, where
another user could plant a symlink for the truncating open. They now live in
$XDG_RUNTIME_DIR only; without one the toggle is ignored and DPMS blanks as
before. The lock file is opened for append, and the lock is taken without a
timeout: the timeout path went ahead unserialized, which was the race the lock
exists to prevent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread test/shell.d/brightness-display-test.sh Resolved
…e lock

The test slept 0.3 s and assumed the background blank had taken the lock by
then; on a loaded runner the wake could finish first and the assertion fail
with the lock working. The ddcutil mock now marks when its delayed write
begins, and the test waits for that mark before sending the wake.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants