fix: use XComposite overlay window for compositor compatibility
- SHA
de34adab58218962f467a0ab939f9bd295fbcea0- Parents
-
86e45ae - Tree
13958e5
de34ada
de34adab58218962f467a0ab939f9bd295fbcea086e45ae
13958e5| Status | File | + | - |
|---|---|---|---|
| M |
Cargo.lock
|
1 | 1 |
| M |
Cargo.toml
|
1 | 1 |
| M |
garlock/src/main.rs
|
60 | 12 |
| M |
garlock/src/screenshot.rs
|
32 | 0 |
| M |
garlock/src/x11/window.rs
|
126 | 4 |
Cargo.lockmodified@@ -523,7 +523,7 @@ dependencies = [ | ||
| 523 | 523 | |
| 524 | 524 | [[package]] |
| 525 | 525 | name = "garlock" |
| 526 | -version = "0.2.0" | |
| 526 | +version = "0.3.0" | |
| 527 | 527 | dependencies = [ |
| 528 | 528 | "anyhow", |
| 529 | 529 | "cairo-rs", |
Cargo.tomlmodified@@ -12,7 +12,7 @@ repository = "https://github.com/mfwolffe/gardesk" | ||
| 12 | 12 | |
| 13 | 13 | [workspace.dependencies] |
| 14 | 14 | # X11 |
| 15 | -x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr"] } | |
| 15 | +x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr", "composite"] } | |
| 16 | 16 | |
| 17 | 17 | # Graphics |
| 18 | 18 | cairo-rs = { version = "0.18", features = ["png"] } |
garlock/src/main.rsmodified@@ -8,6 +8,7 @@ use clap::{Parser, Subcommand}; | ||
| 8 | 8 | use std::path::PathBuf; |
| 9 | 9 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; |
| 10 | 10 | use x11rb::connection::Connection; |
| 11 | +use x11rb::wrapper::ConnectionExt as WrapperConnectionExt; | |
| 11 | 12 | |
| 12 | 13 | mod auth; |
| 13 | 14 | mod background; |
@@ -207,10 +208,12 @@ fn run_lock(config: Config) -> Result<()> { | ||
| 207 | 208 | tracing::info!("Capturing screenshot..."); |
| 208 | 209 | let mut background = match Screenshot::capture() { |
| 209 | 210 | Ok(screenshot) => { |
| 210 | - tracing::debug!( | |
| 211 | + tracing::info!( | |
| 211 | 212 | width = screenshot.width, |
| 212 | 213 | height = screenshot.height, |
| 213 | - "Screenshot captured, applying blur" | |
| 214 | + depth = screenshot.depth, | |
| 215 | + data_len = screenshot.data.len(), | |
| 216 | + "Screenshot captured successfully" | |
| 214 | 217 | ); |
| 215 | 218 | |
| 216 | 219 | // Process with blur and brightness from config |
@@ -219,9 +222,17 @@ fn run_lock(config: Config) -> Result<()> { | ||
| 219 | 222 | config.background.blur_radius, |
| 220 | 223 | config.background.brightness, |
| 221 | 224 | ) { |
| 222 | - Ok(bg) => bg, | |
| 225 | + Ok(bg) => { | |
| 226 | + tracing::info!( | |
| 227 | + bg_width = bg.width, | |
| 228 | + bg_height = bg.height, | |
| 229 | + bg_data_len = bg.data.len(), | |
| 230 | + "Background processed successfully" | |
| 231 | + ); | |
| 232 | + bg | |
| 233 | + } | |
| 223 | 234 | Err(e) => { |
| 224 | - tracing::warn!("Failed to process screenshot: {}, using fallback", e); | |
| 235 | + tracing::error!("Failed to process screenshot: {}, using fallback", e); | |
| 225 | 236 | let (conn, screen_num) = x11rb::connect(None)?; |
| 226 | 237 | let screen = &conn.setup().roots[screen_num]; |
| 227 | 238 | Background::solid_color( |
@@ -233,7 +244,7 @@ fn run_lock(config: Config) -> Result<()> { | ||
| 233 | 244 | } |
| 234 | 245 | } |
| 235 | 246 | Err(e) => { |
| 236 | - tracing::warn!("Failed to capture screenshot: {}, using fallback", e); | |
| 247 | + tracing::error!("Failed to capture screenshot: {}, using fallback", e); | |
| 237 | 248 | let (conn, screen_num) = x11rb::connect(None)?; |
| 238 | 249 | let screen = &conn.setup().roots[screen_num]; |
| 239 | 250 | Background::solid_color( |
@@ -244,15 +255,41 @@ fn run_lock(config: Config) -> Result<()> { | ||
| 244 | 255 | } |
| 245 | 256 | }; |
| 246 | 257 | |
| 258 | + // Step 2: Create fullscreen locker window with input grabs | |
| 259 | + tracing::info!("Creating locker window..."); | |
| 260 | + let locker = LockerWindow::new()?; | |
| 261 | + | |
| 262 | + // Check for size mismatch between background and window | |
| 263 | + let win_width = locker.width() as u32; | |
| 264 | + let win_height = locker.height() as u32; | |
| 265 | + let win_depth = locker.depth(); | |
| 266 | + | |
| 267 | + tracing::info!( | |
| 268 | + win_width, | |
| 269 | + win_height, | |
| 270 | + win_depth, | |
| 271 | + bg_width = background.width, | |
| 272 | + bg_height = background.height, | |
| 273 | + "Window created, checking dimensions" | |
| 274 | + ); | |
| 275 | + | |
| 276 | + // Resize background if dimensions don't match | |
| 277 | + if background.width != win_width || background.height != win_height { | |
| 278 | + tracing::warn!( | |
| 279 | + "Size mismatch! Background {}x{} vs Window {}x{}. Using solid fallback.", | |
| 280 | + background.width, | |
| 281 | + background.height, | |
| 282 | + win_width, | |
| 283 | + win_height | |
| 284 | + ); | |
| 285 | + background = Background::solid_color(win_width, win_height, &config.background.fallback_color); | |
| 286 | + } | |
| 287 | + | |
| 247 | 288 | // Keep a clean copy of the background for re-compositing |
| 248 | 289 | let background_clean = background.data.clone(); |
| 249 | 290 | let bg_width = background.width; |
| 250 | 291 | let bg_height = background.height; |
| 251 | 292 | |
| 252 | - // Step 2: Create fullscreen locker window with input grabs | |
| 253 | - tracing::info!("Creating locker window..."); | |
| 254 | - let locker = LockerWindow::new()?; | |
| 255 | - | |
| 256 | 293 | // Step 3: Initialize keyboard handler with XKB |
| 257 | 294 | tracing::info!("Initializing keyboard handler..."); |
| 258 | 295 | let mut keyboard = Keyboard::new()?; |
@@ -410,8 +447,9 @@ fn run_lock(config: Config) -> Result<()> { | ||
| 410 | 447 | Ok(()) |
| 411 | 448 | }; |
| 412 | 449 | |
| 413 | - // Initial render | |
| 414 | - render_frame( | |
| 450 | + // Initial render - CRITICAL: must complete before event loop | |
| 451 | + tracing::info!("Performing initial render..."); | |
| 452 | + match render_frame( | |
| 415 | 453 | &mut background, |
| 416 | 454 | &background_clean, |
| 417 | 455 | &ring, |
@@ -423,7 +461,17 @@ fn run_lock(config: Config) -> Result<()> { | ||
| 423 | 461 | keyboard.caps_lock_active(), |
| 424 | 462 | locker_state.failed_attempts, |
| 425 | 463 | locker_state.cooldown_remaining(), |
| 426 | - )?; | |
| 464 | + ) { | |
| 465 | + Ok(()) => tracing::info!("Initial render completed successfully"), | |
| 466 | + Err(e) => { | |
| 467 | + tracing::error!("Initial render failed: {}", e); | |
| 468 | + return Err(e); | |
| 469 | + } | |
| 470 | + } | |
| 471 | + | |
| 472 | + // Sync to ensure the image is displayed before continuing | |
| 473 | + locker.conn().sync()?; | |
| 474 | + tracing::debug!("X11 sync completed"); | |
| 427 | 475 | |
| 428 | 476 | // Get current username for PAM authentication |
| 429 | 477 | let username = get_current_username()?; |
garlock/src/screenshot.rsmodified@@ -65,6 +65,24 @@ impl Screenshot { | ||
| 65 | 65 | ); |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | + // Check if screenshot is mostly black (compositor might not be exposing root window) | |
| 69 | + let non_black_pixels = Self::count_non_black_pixels(&reply.data); | |
| 70 | + let total_pixels = (width as usize * height as usize) as f64; | |
| 71 | + let non_black_ratio = non_black_pixels as f64 / total_pixels; | |
| 72 | + | |
| 73 | + if non_black_ratio < 0.01 { | |
| 74 | + tracing::warn!( | |
| 75 | + "Screenshot appears to be mostly black ({:.2}% non-black pixels). \ | |
| 76 | + This may indicate compositor is not exposing root window contents.", | |
| 77 | + non_black_ratio * 100.0 | |
| 78 | + ); | |
| 79 | + } else { | |
| 80 | + tracing::debug!( | |
| 81 | + "Screenshot content check: {:.1}% non-black pixels", | |
| 82 | + non_black_ratio * 100.0 | |
| 83 | + ); | |
| 84 | + } | |
| 85 | + | |
| 68 | 86 | Ok(Self { |
| 69 | 87 | data: reply.data, |
| 70 | 88 | width: width as u32, |
@@ -79,6 +97,20 @@ impl Screenshot { | ||
| 79 | 97 | bgra_to_rgba(&mut rgba); |
| 80 | 98 | rgba |
| 81 | 99 | } |
| 100 | + | |
| 101 | + /// Count pixels that are not pure black (for detecting empty screenshots) | |
| 102 | + fn count_non_black_pixels(data: &[u8]) -> usize { | |
| 103 | + let mut count = 0; | |
| 104 | + // Sample every 100th pixel for performance | |
| 105 | + for chunk in data.chunks_exact(4).step_by(100) { | |
| 106 | + // BGRA format - check if any color channel is non-zero | |
| 107 | + if chunk[0] > 5 || chunk[1] > 5 || chunk[2] > 5 { | |
| 108 | + count += 1; | |
| 109 | + } | |
| 110 | + } | |
| 111 | + // Extrapolate to total | |
| 112 | + count * 100 | |
| 113 | + } | |
| 82 | 114 | } |
| 83 | 115 | |
| 84 | 116 | /// Convert BGRA pixel data to RGBA in-place |
garlock/src/x11/window.rsmodified@@ -2,10 +2,14 @@ | ||
| 2 | 2 | //! |
| 3 | 3 | //! Creates an override-redirect window that covers all monitors |
| 4 | 4 | //! and grabs keyboard/pointer to prevent escape. |
| 5 | +//! | |
| 6 | +//! With compositors, uses the XComposite overlay window to ensure | |
| 7 | +//! the lock screen is rendered above the compositor's output. | |
| 5 | 8 | |
| 6 | 9 | use anyhow::{Context, Result}; |
| 7 | 10 | use std::time::Duration; |
| 8 | 11 | use x11rb::connection::Connection; |
| 12 | +use x11rb::protocol::composite::{self, ConnectionExt as CompositeConnectionExt}; | |
| 9 | 13 | use x11rb::protocol::xproto::*; |
| 10 | 14 | use x11rb::rust_connection::RustConnection; |
| 11 | 15 | use x11rb::wrapper::ConnectionExt as WrapperConnectionExt; |
@@ -22,15 +26,18 @@ pub struct LockerWindow { | ||
| 22 | 26 | width: u16, |
| 23 | 27 | height: u16, |
| 24 | 28 | depth: u8, |
| 29 | + /// Overlay window from XComposite (if available) | |
| 30 | + overlay_window: Option<Window>, | |
| 25 | 31 | } |
| 26 | 32 | |
| 27 | 33 | impl LockerWindow { |
| 28 | 34 | /// Create a new fullscreen locker window |
| 29 | 35 | /// |
| 30 | 36 | /// This will: |
| 31 | - /// 1. Create an override-redirect fullscreen window | |
| 32 | - /// 2. Grab the keyboard (retrying until successful) | |
| 33 | - /// 3. Grab the pointer | |
| 37 | + /// 1. Try to get the XComposite overlay window (for compositor compatibility) | |
| 38 | + /// 2. Create an override-redirect fullscreen window | |
| 39 | + /// 3. Grab the keyboard (retrying until successful) | |
| 40 | + /// 4. Grab the pointer | |
| 34 | 41 | pub fn new() -> Result<Self> { |
| 35 | 42 | let (conn, screen_num) = |
| 36 | 43 | x11rb::connect(None).context("Failed to connect to X server")?; |
@@ -44,12 +51,23 @@ impl LockerWindow { | ||
| 44 | 51 | |
| 45 | 52 | tracing::info!(width, height, "Connected to X server"); |
| 46 | 53 | |
| 54 | + // Try to get the XComposite overlay window for compositor compatibility | |
| 55 | + let overlay_window = Self::get_overlay_window(&conn, root); | |
| 56 | + | |
| 57 | + // Determine parent window - use overlay if available, otherwise root | |
| 58 | + let parent = overlay_window.unwrap_or(root); | |
| 59 | + if overlay_window.is_some() { | |
| 60 | + tracing::info!("Using XComposite overlay window for compositor compatibility"); | |
| 61 | + } else { | |
| 62 | + tracing::debug!("XComposite overlay not available, using root window"); | |
| 63 | + } | |
| 64 | + | |
| 47 | 65 | // Create fullscreen window |
| 48 | 66 | let window = conn.generate_id().context("Failed to generate window ID")?; |
| 49 | 67 | conn.create_window( |
| 50 | 68 | depth, |
| 51 | 69 | window, |
| 52 | - root, | |
| 70 | + parent, | |
| 53 | 71 | 0, |
| 54 | 72 | 0, |
| 55 | 73 | width, |
@@ -84,6 +102,29 @@ impl LockerWindow { | ||
| 84 | 102 | conn.map_window(window).context("Failed to map window")?; |
| 85 | 103 | conn.flush().context("Failed to flush after map")?; |
| 86 | 104 | |
| 105 | + // Wait for MapNotify to ensure window is visible before rendering | |
| 106 | + tracing::debug!("Waiting for window to be mapped..."); | |
| 107 | + let start = std::time::Instant::now(); | |
| 108 | + let timeout = Duration::from_secs(2); | |
| 109 | + let mut mapped = false; | |
| 110 | + | |
| 111 | + while start.elapsed() < timeout { | |
| 112 | + if let Some(event) = conn.poll_for_event()? { | |
| 113 | + if let x11rb::protocol::Event::MapNotify(map_event) = event { | |
| 114 | + if map_event.window == window { | |
| 115 | + mapped = true; | |
| 116 | + tracing::debug!(elapsed_ms = start.elapsed().as_millis(), "Window mapped"); | |
| 117 | + break; | |
| 118 | + } | |
| 119 | + } | |
| 120 | + } | |
| 121 | + std::thread::sleep(Duration::from_millis(1)); | |
| 122 | + } | |
| 123 | + | |
| 124 | + if !mapped { | |
| 125 | + tracing::warn!("MapNotify not received within timeout, continuing anyway"); | |
| 126 | + } | |
| 127 | + | |
| 87 | 128 | // Grab keyboard - CRITICAL for security |
| 88 | 129 | // Must retry because another application might have a grab |
| 89 | 130 | Self::grab_keyboard(&conn, window)?; |
@@ -106,9 +147,63 @@ impl LockerWindow { | ||
| 106 | 147 | width, |
| 107 | 148 | height, |
| 108 | 149 | depth, |
| 150 | + overlay_window, | |
| 109 | 151 | }) |
| 110 | 152 | } |
| 111 | 153 | |
| 154 | + /// Try to get the XComposite overlay window | |
| 155 | + /// | |
| 156 | + /// The overlay window is rendered above the compositor's output, | |
| 157 | + /// which is essential for screen lockers to work with compositors. | |
| 158 | + fn get_overlay_window(conn: &RustConnection, root: Window) -> Option<Window> { | |
| 159 | + // Query Composite extension version | |
| 160 | + let version = match conn.composite_query_version(0, 4) { | |
| 161 | + Ok(cookie) => match cookie.reply() { | |
| 162 | + Ok(reply) => { | |
| 163 | + tracing::debug!( | |
| 164 | + "XComposite version {}.{}", | |
| 165 | + reply.major_version, | |
| 166 | + reply.minor_version | |
| 167 | + ); | |
| 168 | + if reply.major_version == 0 && reply.minor_version < 3 { | |
| 169 | + tracing::warn!("XComposite version too old for overlay window"); | |
| 170 | + return None; | |
| 171 | + } | |
| 172 | + (reply.major_version, reply.minor_version) | |
| 173 | + } | |
| 174 | + Err(e) => { | |
| 175 | + tracing::debug!("Failed to query XComposite version: {}", e); | |
| 176 | + return None; | |
| 177 | + } | |
| 178 | + }, | |
| 179 | + Err(e) => { | |
| 180 | + tracing::debug!("XComposite extension not available: {}", e); | |
| 181 | + return None; | |
| 182 | + } | |
| 183 | + }; | |
| 184 | + | |
| 185 | + // Get the overlay window (requires Composite 0.3+) | |
| 186 | + match conn.composite_get_overlay_window(root) { | |
| 187 | + Ok(cookie) => match cookie.reply() { | |
| 188 | + Ok(reply) => { | |
| 189 | + tracing::info!( | |
| 190 | + overlay_window = reply.overlay_win, | |
| 191 | + "Got XComposite overlay window" | |
| 192 | + ); | |
| 193 | + Some(reply.overlay_win) | |
| 194 | + } | |
| 195 | + Err(e) => { | |
| 196 | + tracing::warn!("Failed to get overlay window: {}", e); | |
| 197 | + None | |
| 198 | + } | |
| 199 | + }, | |
| 200 | + Err(e) => { | |
| 201 | + tracing::warn!("Failed to request overlay window: {}", e); | |
| 202 | + None | |
| 203 | + } | |
| 204 | + } | |
| 205 | + } | |
| 206 | + | |
| 112 | 207 | /// Set fullscreen window hints |
| 113 | 208 | fn set_fullscreen_hints(conn: &RustConnection, window: Window) -> Result<()> { |
| 114 | 209 | let net_wm_state = conn |
@@ -229,6 +324,24 @@ impl LockerWindow { | ||
| 229 | 324 | /// Put an ARGB image to the window |
| 230 | 325 | /// Splits large images into chunks to avoid exceeding X11 request limits |
| 231 | 326 | pub fn put_image(&self, data: &[u8]) -> Result<()> { |
| 327 | + let expected_size = self.width as usize * self.height as usize * 4; | |
| 328 | + if data.len() != expected_size { | |
| 329 | + tracing::error!( | |
| 330 | + "put_image size mismatch: data.len()={} expected={} ({}x{}x4)", | |
| 331 | + data.len(), | |
| 332 | + expected_size, | |
| 333 | + self.width, | |
| 334 | + self.height | |
| 335 | + ); | |
| 336 | + anyhow::bail!( | |
| 337 | + "Image data size {} doesn't match window {}x{} (expected {})", | |
| 338 | + data.len(), | |
| 339 | + self.width, | |
| 340 | + self.height, | |
| 341 | + expected_size | |
| 342 | + ); | |
| 343 | + } | |
| 344 | + | |
| 232 | 345 | let bytes_per_row = self.width as usize * 4; |
| 233 | 346 | let total_rows = self.height as usize; |
| 234 | 347 | |
@@ -239,6 +352,7 @@ impl LockerWindow { | ||
| 239 | 352 | let mut y_offset: i16 = 0; |
| 240 | 353 | let mut remaining_rows = total_rows; |
| 241 | 354 | let mut data_offset = 0; |
| 355 | + let mut chunks_sent = 0; | |
| 242 | 356 | |
| 243 | 357 | while remaining_rows > 0 { |
| 244 | 358 | let chunk_rows = remaining_rows.min(rows_per_chunk); |
@@ -263,9 +377,11 @@ impl LockerWindow { | ||
| 263 | 377 | y_offset += chunk_rows as i16; |
| 264 | 378 | remaining_rows -= chunk_rows; |
| 265 | 379 | data_offset += chunk_bytes; |
| 380 | + chunks_sent += 1; | |
| 266 | 381 | } |
| 267 | 382 | |
| 268 | 383 | self.conn.flush().context("Failed to flush after put_image")?; |
| 384 | + tracing::trace!(chunks_sent, "put_image completed"); | |
| 269 | 385 | Ok(()) |
| 270 | 386 | } |
| 271 | 387 | |
@@ -289,6 +405,12 @@ impl Drop for LockerWindow { | ||
| 289 | 405 | let _ = self.conn.ungrab_pointer(CURRENT_TIME); |
| 290 | 406 | // Destroy window |
| 291 | 407 | let _ = self.conn.destroy_window(self.window); |
| 408 | + // Release overlay window if we acquired it | |
| 409 | + if let Some(overlay) = self.overlay_window { | |
| 410 | + let root = self.conn.setup().roots[self.screen_num].root; | |
| 411 | + let _ = self.conn.composite_release_overlay_window(root); | |
| 412 | + tracing::debug!(overlay, "Released XComposite overlay window"); | |
| 413 | + } | |
| 292 | 414 | let _ = self.conn.flush(); |
| 293 | 415 | tracing::debug!("Locker window destroyed, grabs released"); |
| 294 | 416 | } |