gardesk/garlock / de34ada

Browse files

fix: use XComposite overlay window for compositor compatibility

Authored by espadonne
SHA
de34adab58218962f467a0ab939f9bd295fbcea0
Parents
86e45ae
Tree
13958e5

5 changed files

StatusFile+-
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 = [
523523
 
524524
 [[package]]
525525
 name = "garlock"
526
-version = "0.2.0"
526
+version = "0.3.0"
527527
 dependencies = [
528528
  "anyhow",
529529
  "cairo-rs",
Cargo.tomlmodified
@@ -12,7 +12,7 @@ repository = "https://github.com/mfwolffe/gardesk"
1212
 
1313
 [workspace.dependencies]
1414
 # X11
15
-x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr"] }
15
+x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr", "composite"] }
1616
 
1717
 # Graphics
1818
 cairo-rs = { version = "0.18", features = ["png"] }
garlock/src/main.rsmodified
@@ -8,6 +8,7 @@ use clap::{Parser, Subcommand};
88
 use std::path::PathBuf;
99
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
1010
 use x11rb::connection::Connection;
11
+use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
1112
 
1213
 mod auth;
1314
 mod background;
@@ -207,10 +208,12 @@ fn run_lock(config: Config) -> Result<()> {
207208
     tracing::info!("Capturing screenshot...");
208209
     let mut background = match Screenshot::capture() {
209210
         Ok(screenshot) => {
210
-            tracing::debug!(
211
+            tracing::info!(
211212
                 width = screenshot.width,
212213
                 height = screenshot.height,
213
-                "Screenshot captured, applying blur"
214
+                depth = screenshot.depth,
215
+                data_len = screenshot.data.len(),
216
+                "Screenshot captured successfully"
214217
             );
215218
 
216219
             // Process with blur and brightness from config
@@ -219,9 +222,17 @@ fn run_lock(config: Config) -> Result<()> {
219222
                 config.background.blur_radius,
220223
                 config.background.brightness,
221224
             ) {
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
+                }
223234
                 Err(e) => {
224
-                    tracing::warn!("Failed to process screenshot: {}, using fallback", e);
235
+                    tracing::error!("Failed to process screenshot: {}, using fallback", e);
225236
                     let (conn, screen_num) = x11rb::connect(None)?;
226237
                     let screen = &conn.setup().roots[screen_num];
227238
                     Background::solid_color(
@@ -233,7 +244,7 @@ fn run_lock(config: Config) -> Result<()> {
233244
             }
234245
         }
235246
         Err(e) => {
236
-            tracing::warn!("Failed to capture screenshot: {}, using fallback", e);
247
+            tracing::error!("Failed to capture screenshot: {}, using fallback", e);
237248
             let (conn, screen_num) = x11rb::connect(None)?;
238249
             let screen = &conn.setup().roots[screen_num];
239250
             Background::solid_color(
@@ -244,15 +255,41 @@ fn run_lock(config: Config) -> Result<()> {
244255
         }
245256
     };
246257
 
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
+
247288
     // Keep a clean copy of the background for re-compositing
248289
     let background_clean = background.data.clone();
249290
     let bg_width = background.width;
250291
     let bg_height = background.height;
251292
 
252
-    // Step 2: Create fullscreen locker window with input grabs
253
-    tracing::info!("Creating locker window...");
254
-    let locker = LockerWindow::new()?;
255
-
256293
     // Step 3: Initialize keyboard handler with XKB
257294
     tracing::info!("Initializing keyboard handler...");
258295
     let mut keyboard = Keyboard::new()?;
@@ -410,8 +447,9 @@ fn run_lock(config: Config) -> Result<()> {
410447
         Ok(())
411448
     };
412449
 
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(
415453
         &mut background,
416454
         &background_clean,
417455
         &ring,
@@ -423,7 +461,17 @@ fn run_lock(config: Config) -> Result<()> {
423461
         keyboard.caps_lock_active(),
424462
         locker_state.failed_attempts,
425463
         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");
427475
 
428476
     // Get current username for PAM authentication
429477
     let username = get_current_username()?;
garlock/src/screenshot.rsmodified
@@ -65,6 +65,24 @@ impl Screenshot {
6565
             );
6666
         }
6767
 
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
+
6886
         Ok(Self {
6987
             data: reply.data,
7088
             width: width as u32,
@@ -79,6 +97,20 @@ impl Screenshot {
7997
         bgra_to_rgba(&mut rgba);
8098
         rgba
8199
     }
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
+    }
82114
 }
83115
 
84116
 /// Convert BGRA pixel data to RGBA in-place
garlock/src/x11/window.rsmodified
@@ -2,10 +2,14 @@
22
 //!
33
 //! Creates an override-redirect window that covers all monitors
44
 //! 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.
58
 
69
 use anyhow::{Context, Result};
710
 use std::time::Duration;
811
 use x11rb::connection::Connection;
12
+use x11rb::protocol::composite::{self, ConnectionExt as CompositeConnectionExt};
913
 use x11rb::protocol::xproto::*;
1014
 use x11rb::rust_connection::RustConnection;
1115
 use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
@@ -22,15 +26,18 @@ pub struct LockerWindow {
2226
     width: u16,
2327
     height: u16,
2428
     depth: u8,
29
+    /// Overlay window from XComposite (if available)
30
+    overlay_window: Option<Window>,
2531
 }
2632
 
2733
 impl LockerWindow {
2834
     /// Create a new fullscreen locker window
2935
     ///
3036
     /// 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
3441
     pub fn new() -> Result<Self> {
3542
         let (conn, screen_num) =
3643
             x11rb::connect(None).context("Failed to connect to X server")?;
@@ -44,12 +51,23 @@ impl LockerWindow {
4451
 
4552
         tracing::info!(width, height, "Connected to X server");
4653
 
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
+
4765
         // Create fullscreen window
4866
         let window = conn.generate_id().context("Failed to generate window ID")?;
4967
         conn.create_window(
5068
             depth,
5169
             window,
52
-            root,
70
+            parent,
5371
             0,
5472
             0,
5573
             width,
@@ -84,6 +102,29 @@ impl LockerWindow {
84102
         conn.map_window(window).context("Failed to map window")?;
85103
         conn.flush().context("Failed to flush after map")?;
86104
 
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
+
87128
         // Grab keyboard - CRITICAL for security
88129
         // Must retry because another application might have a grab
89130
         Self::grab_keyboard(&conn, window)?;
@@ -106,9 +147,63 @@ impl LockerWindow {
106147
             width,
107148
             height,
108149
             depth,
150
+            overlay_window,
109151
         })
110152
     }
111153
 
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
+
112207
     /// Set fullscreen window hints
113208
     fn set_fullscreen_hints(conn: &RustConnection, window: Window) -> Result<()> {
114209
         let net_wm_state = conn
@@ -229,6 +324,24 @@ impl LockerWindow {
229324
     /// Put an ARGB image to the window
230325
     /// Splits large images into chunks to avoid exceeding X11 request limits
231326
     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
+
232345
         let bytes_per_row = self.width as usize * 4;
233346
         let total_rows = self.height as usize;
234347
 
@@ -239,6 +352,7 @@ impl LockerWindow {
239352
         let mut y_offset: i16 = 0;
240353
         let mut remaining_rows = total_rows;
241354
         let mut data_offset = 0;
355
+        let mut chunks_sent = 0;
242356
 
243357
         while remaining_rows > 0 {
244358
             let chunk_rows = remaining_rows.min(rows_per_chunk);
@@ -263,9 +377,11 @@ impl LockerWindow {
263377
             y_offset += chunk_rows as i16;
264378
             remaining_rows -= chunk_rows;
265379
             data_offset += chunk_bytes;
380
+            chunks_sent += 1;
266381
         }
267382
 
268383
         self.conn.flush().context("Failed to flush after put_image")?;
384
+        tracing::trace!(chunks_sent, "put_image completed");
269385
         Ok(())
270386
     }
271387
 
@@ -289,6 +405,12 @@ impl Drop for LockerWindow {
289405
         let _ = self.conn.ungrab_pointer(CURRENT_TIME);
290406
         // Destroy window
291407
         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
+        }
292414
         let _ = self.conn.flush();
293415
         tracing::debug!("Locker window destroyed, grabs released");
294416
     }