Skip to content

Fix/idle compositor redraw loop + get back input devices after hotplug#30

Merged
joske merged 3 commits into
masterfrom
fix/idle-compositor
Jun 14, 2026
Merged

Fix/idle compositor redraw loop + get back input devices after hotplug#30
joske merged 3 commits into
masterfrom
fix/idle-compositor

Conversation

@joske

@joske joske commented Jun 14, 2026

Copy link
Copy Markdown
Owner

fix: stop idle-compositor redraw loop + idle wakeups on master-drop
fix: re-acquire input devices after a deferred libseat open (hotplug retry)

joske and others added 3 commits June 14, 2026 14:27
An idle desktop never stopped compositing (~43 fps continuously): build_scene
damaged the cursor rect every frame regardless of movement, so the empty-damage
skip in tick_one_output was unreachable whenever a cursor was on-screen.
Continuous full-screen compose is the GPU-hammer behind the overnight
VK_ERROR_DEVICE_LOST.

Part A — cursor damage is change-gated via a per-output last_present_cursor
footprint (all modes, advanced transactionally on retire). Damage = old ∪ new
emitted only when the footprint/mode/sprite changed; a stationary same-mode
cursor contributes nothing. The footprint also supplies the non-empty poke that
carries Hw→Hidden's HideOnRetire (previously stranded, since the HW current-rect
lived only in the visible branch).

Part B — tick() clears scene_structure_dirty only when every output was Composed
or SkippedEmpty (keeps it set on any defer skip or error). Fixes the
dirty-but-empty hot-spin where a bare wake_for_damage / register_cursor left the
flag set and next_wakeup spun.

Part C — next_wakeup suppresses the scene + cursor-anim deadlines when DRM master
is dropped or outputs are DPMS-off, but keeps the present-poll deadline
(PresentBatchWait::Poll batches have no fd wake).

Spec + 4-round codex review:
docs/superpowers/specs/2026-06-14-idle-compositor-cursor-damage.md
Unit coverage: cursor-damage gating (stationary / moved / sprite-swap / pure-HW
hide), dirty-clear classification, scanout-disallowed wakeup suppression,
present-poll survival under DPMS-off.

HW (silence, e16, 2560x1440@60): survived a real monitor off->on cycle at
<=22 fps with no storm / DEVICE_LOST / EINVAL (was a 14000-draw/s busy-loop
before). Residual idle-rate confirmation pending a clean run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…retry)

When the monitor's USB hub power-cycles (screen off→on), the kernel removes
and re-enumerates the keyboard + mouse. yserver picked up the keyboard but the
mouse stayed dead for many seconds — until an unrelated keypress — both under
libseat (Cinnamon) and direct (lightdm) mode.

Root cause (HW-traced on bee, confirmed by udevadm-vs-yserver timestamps): the
re-enumerated device's libseat/logind open is DEFERRED by a lagging udev uaccess
ACL, so libinput surfaces no DEVICE_ADDED on that wake. There is no later fd
readiness edge once the ACL settles, so the idle core loop (and the direct-mode
input thread, blocked on EpollTimeout::NONE) never re-dispatches libinput to
retry the open — it waits for unrelated input. The recently-landed idle-sleep
made this visible; it is a latent libinput-hotplug bug, not a regression of the
compose path.

Fix — a bounded retry window armed on any libinput device add/remove, so a
deferred open is retried without needing unrelated input, then it converges and
the loop returns to true idle:

- Libseat (core loop): new `KmsBackendV2.libinput_hotplug_retry_until`;
  `on_libinput_ready` arms it (2.5s) on DeviceAdded/Removed; new
  `Backend::poll_deferred_input` (default no-op; called once per core-loop
  iteration in run.rs) re-dispatches while armed and clears on expiry;
  `next_wakeup` adds a ~250ms retry cadence while armed (ungated by scanout, so
  input recovers regardless of screen state). Only device churn extends the
  window → idle-sleep preserved.
- Direct (input_thread.rs): the loop blocked on EpollTimeout::NONE forever; now
  epoll_wait takes a ~250ms timeout while a retry window is armed (armed on a
  batch containing DeviceAdded/Removed), so the existing per-iteration dispatch
  retries the deferred open. (Level-triggered epoll alone was insufficient — the
  udev event is consumed without surfacing the add.)

Unit tests cover the scheduling seam (retry cadence in next_wakeup, window
cleared on expiry). HW-verified on bee: monitor power-cycle under both Cinnamon
(libseat) and lightdm (direct) — mouse returns on its own within the retry
window, no keypress, and compose returns to 0 fps afterward (idle preserved).

Also adds repro/diagnostic Justfile recipes: yserver-cinnamon-hotplug-probe,
yserver-input-hotplug-probe, yserver-fvwm3-idle, yserver-e16-idle-trace,
xorg-e16-idle-trace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nic!

The idle-compose fix added a `#[ignore]`d placeholder whose body was
`panic!("enable this when Repaint::Clipped returns again")`. Plain `cargo test`
skips it, but CI runs `--include-ignored`, which force-runs it → guaranteed
failure.

Replace it with a real, non-ignored test that asserts `pick_repaint_region`
returns `Repaint::Full` unconditionally today (it does) and carries the
cursor-specific message. It passes now and fails — pointing at the requirement —
the day `Repaint::Clipped` is re-enabled, which is exactly the tripwire the
idle-compose spec wanted, without breaking `--include-ignored`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joske joske merged commit 2d1bdc6 into master Jun 14, 2026
1 check passed
@joske joske deleted the fix/idle-compositor branch June 14, 2026 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant