Commit 47b86d08d05a
tarball9 files changed+196-12
@@ -49,6 +49,12 @@ style = "orthogonal"
4949
phase_ms = 1605050
curve = [0.45, 0.0, 0.2, 1.0]5151
52
+[idle]53
+# Seconds without input before every output is switched off, any input54
+# switches them back on. 0 never blanks. Windows holding an idle inhibitor55
+# keep the screen on while they are shown.56
+blank_after = 057
+5258
[keyboard]5359
layout = ""5460
repeat_delay = 200@@ -17,6 +17,7 @@ pub struct Config {
1717
pub animation: Animation,1818
pub keyboard: Keyboard,1919
pub input: Input,20
+ pub idle: Idle,2021
pub binds: Vec<Bind>,2122
pub rules: Vec<Rule>,2223
pub startup: Vec<Vec<String>>,@@ -235,6 +236,14 @@ pub enum AnimationStyle {
235236
Direct,236237
}237238
239
+#[derive(Deserialize, Debug, Clone, PartialEq, Default)]240
+#[serde(default, deny_unknown_fields)]241
+pub struct Idle {242
+ /// Seconds without input before every output is switched off, 0 for243
+ /// never.244
+ pub blank_after: u64,245
+}246
+238247
#[derive(Deserialize, Debug, Clone, PartialEq)]239248
#[serde(default, deny_unknown_fields)]240249
pub struct Keyboard {@@ -512,6 +521,7 @@ impl Config {
512521
let general: General = section("general", table.remove("general"), w);513522
let keyboard = section("keyboard", table.remove("keyboard"), w);514523
let input = input(table.remove("input"), w);524
+ let idle = section("idle", table.remove("idle"), w);515525
let animation = section("animation", table.remove("animation"), w);516526
let decoration = section("decoration", table.remove("decoration"), w);517527
let output: Outputs = section("output", table.remove("output"), w);@@ -571,6 +581,7 @@ impl Config {
571581
output,572582
keyboard,573583
input,584
+ idle,574585
binds,575586
rules,576587
startup,@@ -759,6 +770,7 @@ mod tests {
759770
assert_eq!(c.animation, Animation::default());760771
assert_eq!(c.keyboard, Keyboard::default());761772
assert_eq!(c.input, Input::default());773
+ assert_eq!(c.idle, Idle::default());762774
assert!(c.binds.len() > 40);763775
}764776
@@ -229,6 +229,7 @@ impl State {
229229
drm.devices.reconfigure(&self.settings.config.input);230230
}231231
crate::drm::reconfigure_outputs(self);232
+ self.configure_idle();232233
tracing::info!("config reloaded");233234
Ok(())234235
}@@ -83,6 +83,8 @@ struct Surface {
8383
/// A frame is on its way to the screen and its vblank has not landed.8484
queued: bool,8585
queued_at: u64,86
+ /// The panel is lit. Off outputs are not rendered to.87
+ powered: bool,8688
/// Fires when a queued frame's vblank never arrives.8789
watchdog: Option<RegistrationToken>,8890
/// Consecutive frames that failed to render or queue.@@ -221,6 +223,11 @@ pub fn init(
221223
s.target_ns = None;222224
s.failures = 0;223225
s.redraw = true;226
+ if !s.powered227
+ && let Err(e) = s.drm.with_compositor(|c| c.clear())228
+ {229
+ tracing::warn!("could not keep the output off: {e}");230
+ }224231
}225232
}226233
scan(state);@@ -310,6 +317,7 @@ fn scan(state: &mut State) {
310317
for handle in retry {311318
retry_connector(state, handle);312319
}320
+ state.apply_power();313321
state.apply_layout();314322
}315323
@@ -471,6 +479,7 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
471479
drm: out,472480
queued: false,473481
queued_at: 0,482
+ powered: true,474483
watchdog: None,475484
failures: 0,476485
redraw: true,@@ -622,9 +631,43 @@ fn disconnect(state: &mut State, crtc: crtc::Handle) {
622631
state.wm.layout.remove_output(surface.id);623632
state.close_layers(&surface.output);624633
state.services.lock.forget(&surface.output);634
+ state.services.power.forget(&surface.output.name());625635
}626636
627637
impl Drm {638
+ /// Lights or darkens the output called `name`. Dark outputs get DPMS off639
+ /// and no frames, and the next frame after turning on is a full modeset.640
+ pub fn set_powered(&mut self, name: &str, on: bool) {641
+ let active = self.session.is_active();642
+ for s in self643
+ .surfaces644
+ .values_mut()645
+ .filter(|s| s.output.name() == name)646
+ {647
+ if s.powered == on {648
+ continue;649
+ }650
+ s.powered = on;651
+ tracing::info!(name, on, "output power");652
+ if on {653
+ s.redraw = true;654
+ continue;655
+ }656
+ for token in [s.scheduled.take(), s.watchdog.take()]657
+ .into_iter()658
+ .flatten()659
+ {660
+ self.handle.remove(token);661
+ }662
+ s.queued = false;663
+ s.target_ns = None;664
+ s.cursor_redraw = false;665
+ if active && let Err(e) = s.drm.with_compositor(|c| c.clear()) {666
+ tracing::warn!(name, "could not switch the output off: {e}");667
+ }668
+ }669
+ }670
+628671
pub fn output(&self, id: OutputId) -> Option<Output> {629672
self.surfaces630673
.values()@@ -713,7 +756,7 @@ fn schedule(state: &mut State, crtc: crtc::Handle) {
713756
let Some(surface) = drm.surfaces.get_mut(&crtc) else {714757
return;715758
};716
- if surface.queued || surface.scheduled.is_some() {759
+ if !surface.powered || surface.queued || surface.scheduled.is_some() {717760
return;718761
}719762
let now = now_ns();@@ -918,7 +961,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
918961
let Some(surface) = drm.surfaces.get_mut(&crtc) else {919962
return false;920963
};921
- if surface.queued {964
+ if surface.queued || !surface.powered {922965
return false;923966
}924967
surface.cursor_redraw = false;@@ -45,11 +45,6 @@ impl IdleInhibitHandler for State {
4545
}4646
4747
impl State {48
- pub fn activity(&mut self) {49
- let seat = self.input.seat.clone();50
- self.services.idle.notifier.notify_activity(&seat);51
- }52
-5348
/// An inhibitor only counts while its surface is on screen, as the5449
/// protocol asks, so a video on a hidden tag lets the screen blank.5550
pub fn update_inhibit(&mut self) {@@ -67,6 +62,9 @@ impl State {
6762
});6863
if shown != self.services.idle.inhibited {6964
self.services.idle.inhibited = shown;65
+ if !shown {66
+ self.services.power.touch();67
+ }7068
self.services.idle.notifier.set_is_inhibited(shown);7169
}7270
}@@ -108,11 +108,10 @@ impl State {
108108
if self.services.lock.pending.is_none() {109109
return;110110
}111
- let all = self112
- .wm113
- .space114
- .outputs()115
- .all(|o| self.services.lock.blanked.contains(&o.name()));111
+ let all = self.wm.space.outputs().all(|o| {112
+ let name = o.name();113
+ self.services.lock.blanked.contains(&name) || !self.output_powered(&name)114
+ });116115
if all && let Some(locker) = self.services.lock.pending.take() {117116
locker.lock();118117
self.services.lock.locked = true;@@ -19,6 +19,7 @@ mod layers;
1919
mod lock;2020
mod nested;2121
mod pacing;22
+mod power;2223
mod scene;2324
mod seat;2425
mod state;@@ -264,6 +265,7 @@ fn run(
264265
}265266
ipc::init(&event_loop, &mut state)?;266267
xwayland::init(&mut state);268
+ state.configure_idle();267269
tracing::info!(socket = ?state.core.socket_name, device = state.renderer.device_name(), "monolith running");268270
269271
if gpu.is_some() {@@ -0,0 +1,121 @@
1
+//! Whether each output is lit. Idle blanking, the ipc `power` command and2
+//! wlr-output-power-management all end up here, and the backend is told what3
+//! came out.4
+5
+use crate::state::{State, now_ns};6
+use smithay::reexports::calloop::RegistrationToken;7
+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};8
+use std::collections::HashSet;9
+use std::time::Duration;10
+11
+#[derive(Default)]12
+pub struct Power {13
+ /// Every output is dark until the next input.14
+ idle_blanked: bool,15
+ /// Outputs switched off by name, which input does not wake.16
+ off: HashSet<String>,17
+ last_activity: u64,18
+ timer: Option<RegistrationToken>,19
+}20
+21
+impl Power {22
+ pub fn dark(&self, name: &str) -> bool {23
+ self.idle_blanked || self.off.contains(name)24
+ }25
+26
+ /// An inhibitor going away counts as activity, so the screen does not27
+ /// blank the moment a video stops.28
+ pub fn touch(&mut self) {29
+ self.last_activity = now_ns();30
+ }31
+32
+ pub fn forget(&mut self, name: &str) {33
+ self.off.remove(name);34
+ }35
+}36
+37
+impl State {38
+ pub fn output_powered(&self, name: &str) -> bool {39
+ !self.services.power.dark(name)40
+ }41
+42
+ /// Pushes the wanted power state of every output to the backend.43
+ pub fn apply_power(&mut self) {44
+ let names: Vec<String> = self.wm.space.outputs().map(|o| o.name()).collect();45
+ for name in names {46
+ let on = self.output_powered(&name);47
+ if let Some(drm) = self.backend.drm_mut() {48
+ drm.set_powered(&name, on);49
+ }50
+ }51
+ }52
+53
+ /// Notes input, and lights the outputs again if idleness had blanked them.54
+ pub fn activity(&mut self) {55
+ let seat = self.input.seat.clone();56
+ self.services.idle.notifier.notify_activity(&seat);57
+ self.services.power.touch();58
+ if self.services.power.idle_blanked {59
+ self.wake_idle();60
+ self.apply_power();61
+ }62
+ }63
+64
+ fn wake_idle(&mut self) {65
+ if std::mem::take(&mut self.services.power.idle_blanked) {66
+ tracing::info!("input after idle, outputs back on");67
+ self.configure_idle();68
+ }69
+ }70
+71
+ /// Arms the idle timer from the config, or disarms it.72
+ pub fn configure_idle(&mut self) {73
+ if let Some(token) = self.services.power.timer.take() {74
+ self.core.handle.remove(token);75
+ }76
+ let secs = self.settings.config.idle.blank_after;77
+ if secs == 0 || self.services.power.idle_blanked {78
+ return;79
+ }80
+ let power = &mut self.services.power;81
+ if power.last_activity == 0 {82
+ power.touch();83
+ }84
+ let timeout = secs.saturating_mul(1_000_000_000);85
+ let left = timeout.saturating_sub(now_ns().saturating_sub(power.last_activity));86
+ let after = Timer::from_duration(Duration::from_nanos(left.max(1_000_000)));87
+ let token = self88
+ .core89
+ .handle90
+ .insert_source(after, |_, _, state| state.idle_elapsed());91
+ match token {92
+ Ok(token) => self.services.power.timer = Some(token),93
+ Err(e) => tracing::warn!("could not arm the idle timer: {e}"),94
+ }95
+ }96
+97
+ fn idle_elapsed(&mut self) -> TimeoutAction {98
+ let timeout = self99
+ .settings100
+ .config101
+ .idle102
+ .blank_after103
+ .saturating_mul(1_000_000_000);104
+ if timeout == 0 {105
+ self.services.power.timer = None;106
+ return TimeoutAction::Drop;107
+ }108
+ let idle_for = now_ns().saturating_sub(self.services.power.last_activity);109
+ if idle_for < timeout {110
+ return TimeoutAction::ToDuration(Duration::from_nanos(timeout - idle_for));111
+ }112
+ if self.services.idle.inhibited {113
+ return TimeoutAction::ToDuration(Duration::from_nanos(timeout));114
+ }115
+ tracing::info!("idle, switching outputs off");116
+ self.services.power.timer = None;117
+ self.services.power.idle_blanked = true;118
+ self.apply_power();119
+ TimeoutAction::Drop120
+ }121
+}@@ -88,6 +88,7 @@ pub struct Services {
8888
pub launches: crate::launch::Launches,8989
pub ipc: crate::ipc::Server,9090
pub idle: crate::idle::Idle,91
+ pub power: crate::power::Power,9192
pub commits: crate::stats::CommitStats,9293
}9394
@@ -199,6 +200,7 @@ impl State {
199200
launches: Default::default(),200201
ipc: Default::default(),201202
idle,203
+ power: Default::default(),202204
commits: Default::default(),203205
},204206
})