fix: handle dynamic output changes while session is locked

When monitors are powered off (e.g. via niri power-off-monitors or
physical power switches), niri destroys and re-advertises the Wayland
outputs as they come back. Previously all three OutputHandler callbacks
were no-ops, causing two bugs:

- new_output: no lock surface was created for outputs that appeared
  after the initial lock, so the slowest monitor to wake up would show
  the compositor's red fallback instead of the lock screen.
- output_destroyed: stale LockedSurface, SessionLockSurface, output,
  and captured_background entries accumulated for gone outputs.

Fix new_output to create a lock surface (and register it with the lock
manager) whenever a new output appears while the session is locked.

Fix output_destroyed to remove the corresponding entries from
lock_surfaces, lock_manager.surfaces, outputs, and
captured_backgrounds, keeping all parallel vecs in sync. Add
LockManager::remove_surface_by_output to support this, returning the
removal index so lock_surfaces can be updated with the same index.

The all-monitors-off scenario (all outputs destroyed simultaneously)
is handled naturally: the vecs are emptied and repopulated as each
monitor fires new_output on wake.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthias Hörmann
2026-03-27 12:57:20 +01:00
parent 9171f0771a
commit f8c4381a11
2 changed files with 66 additions and 3 deletions
+11
View File
@@ -262,6 +262,17 @@ impl LockManager {
action
}
pub fn remove_surface_by_output(&mut self, output: &wl_output::WlOutput) -> Option<usize> {
use wayland_client::Proxy;
let output_id = Proxy::id(output);
let idx = self
.surfaces
.iter()
.position(|s| Proxy::id(s.output()) == output_id)?;
self.surfaces.remove(idx);
Some(idx)
}
pub fn set_system_status(&mut self, status: SystemStatus) {
for surface in &mut self.surfaces {
surface.set_system_status(status.clone());
+55 -3
View File
@@ -296,9 +296,61 @@ impl OutputHandler for WaylandLock {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: WlOutput) {}
fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: WlOutput) {}
fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: WlOutput) {
fn new_output(&mut self, _conn: &Connection, qh: &QueueHandle<Self>, output: WlOutput) {
// If we are already locked, we must create a lock surface for this newly
// available output. This happens e.g. when a monitor powers back on after
// "niri msg action power-off-monitors" — niri re-advertises the output and
// the compositor requires a lock surface on every output or it shows a
// compositor-defined fallback (typically a solid red/black screen).
if let Some(session_lock) = &self.session_lock {
let surface = self.compositor_state.create_surface(qh);
let (width, height) = self.get_output_dimensions(&output);
let lock_surface = session_lock.create_lock_surface(surface.clone(), &output, qh);
self.lock_surfaces.push(lock_surface);
if !self.outputs.contains(&output) {
self.outputs.push(output.clone());
}
if let Ok(mut lm) = self.lock_manager.lock() {
lm.add_surface(width, height, output);
let count = lm.surface_count();
if let Some(ls) = lm.get_surface_mut(count - 1) {
ls.set_wayland_surface(surface);
}
}
log::info!("Created lock surface for newly available output");
}
}
fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: WlOutput) {
// Dimension changes while locked are handled by the compositor sending a configure
// event on the lock surface, which the SessionLockHandler::configure callback
// already processes via locked_surface.resize().
}
fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
// Clean up the lock surface and associated state for this output.
// This happens e.g. when a monitor is powered off with its physical power switch.
// When the monitor comes back on, new_output() will fire and recreate everything.
//
// outputs and captured_backgrounds are kept at the same indices, so we remove
// from both using the same position.
let output_id = Proxy::id(&output);
if let Some(idx) = self.outputs.iter().position(|o| Proxy::id(o) == output_id) {
self.outputs.remove(idx);
if idx < self.captured_backgrounds.len() {
self.captured_backgrounds.remove(idx);
}
}
// lock_manager.surfaces and lock_surfaces are built in tandem and share indices,
// so the index returned from the lock_manager removal applies to lock_surfaces too.
if let Ok(mut lm) = self.lock_manager.lock() {
if let Some(idx) = lm.remove_surface_by_output(&output) {
if idx < self.lock_surfaces.len() {
drop(self.lock_surfaces.remove(idx));
}
}
}
log::info!("Removed lock surface for destroyed output");
}
}