gardesk/garshot / d694d2d

Browse files

annotate: add chunked put_image for large screenshots

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
d694d2d1ea614de7b6d3c223fdbac0f71581224c
Parents
431fa48
Tree
7f5bcd2

1 changed file

StatusFile+-
M garshot/src/annotate/overlay.rs 136 26
garshot/src/annotate/overlay.rsmodified
@@ -9,11 +9,15 @@ use crate::annotate::tools::{self, Tool};
9
 use crate::annotate::ui::{Toolbar, TOOLBAR_HEIGHT};
9
 use crate::annotate::ui::{Toolbar, TOOLBAR_HEIGHT};
10
 
10
 
11
 use gartk_core::{InputEvent, Key, KeyEvent, Modifiers, MouseButton, MouseEvent, Point};
11
 use gartk_core::{InputEvent, Key, KeyEvent, Modifiers, MouseButton, MouseEvent, Point};
12
+use gartk_render::Surface;
12
 use gartk_x11::{Connection, CursorManager, Window, WindowConfig};
13
 use gartk_x11::{Connection, CursorManager, Window, WindowConfig};
13
 use x11rb::connection::Connection as X11Connection;
14
 use x11rb::connection::Connection as X11Connection;
14
-use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt, PropMode};
15
+use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt, ImageFormat, PropMode};
15
 use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
16
 use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
16
 
17
 
18
+/// Maximum bytes per put_image request (conservative, below typical 256KB limit).
19
+const MAX_PUT_IMAGE_BYTES: usize = 65536;
20
+
17
 /// Annotation overlay for editing screenshots.
21
 /// Annotation overlay for editing screenshots.
18
 pub struct AnnotationOverlay {
22
 pub struct AnnotationOverlay {
19
     /// X11 connection.
23
     /// X11 connection.
@@ -32,6 +36,8 @@ pub struct AnnotationOverlay {
32
     history: History,
36
     history: History,
33
     /// Toolbar UI.
37
     /// Toolbar UI.
34
     toolbar: Toolbar,
38
     toolbar: Toolbar,
39
+    /// Toolbar surface (reused to avoid allocation each frame).
40
+    toolbar_surface: Surface,
35
     /// Cursor manager.
41
     /// Cursor manager.
36
     cursor_manager: CursorManager,
42
     cursor_manager: CursorManager,
37
     /// Whether a redraw is needed.
43
     /// Whether a redraw is needed.
@@ -48,12 +54,16 @@ impl AnnotationOverlay {
48
     /// * `width` - Image width
54
     /// * `width` - Image width
49
     /// * `height` - Image height
55
     /// * `height` - Image height
50
     pub fn new(image_data: &[u8], width: u32, height: u32) -> Result<Self> {
56
     pub fn new(image_data: &[u8], width: u32, height: u32) -> Result<Self> {
57
+        tracing::debug!("Creating annotation overlay for image {}x{}", width, height);
58
+
51
         // Connect to X11
59
         // Connect to X11
52
         let conn = Connection::connect(None).context("Failed to connect to X11")?;
60
         let conn = Connection::connect(None).context("Failed to connect to X11")?;
53
 
61
 
54
         // Window size = image size + toolbar height
62
         // Window size = image size + toolbar height
55
         let window_width = width;
63
         let window_width = width;
56
         let window_height = height + TOOLBAR_HEIGHT;
64
         let window_height = height + TOOLBAR_HEIGHT;
65
+        tracing::debug!("Window size will be {}x{} (including {}px toolbar)",
66
+            window_width, window_height, TOOLBAR_HEIGHT);
57
 
67
 
58
         // Center window on screen
68
         // Center window on screen
59
         let screen_width = conn.screen_width();
69
         let screen_width = conn.screen_width();
@@ -70,6 +80,8 @@ impl AnnotationOverlay {
70
             .map_on_create(false);
80
             .map_on_create(false);
71
 
81
 
72
         let window = Window::create(conn.clone(), config).context("Failed to create window")?;
82
         let window = Window::create(conn.clone(), config).context("Failed to create window")?;
83
+        tracing::debug!("Created window {} at position ({}, {})",
84
+            window.id(), pos_x.max(0), pos_y.max(0));
73
 
85
 
74
         // Set window type to DIALOG so gar floats it automatically
86
         // Set window type to DIALOG so gar floats it automatically
75
         let window_type_atom = conn.inner()
87
         let window_type_atom = conn.inner()
@@ -109,6 +121,47 @@ impl AnnotationOverlay {
109
         // Create initial tool
121
         // Create initial tool
110
         let tool = tools::create_tool(ToolType::Arrow);
122
         let tool = tools::create_tool(ToolType::Arrow);
111
 
123
 
124
+        // Create reusable toolbar surface
125
+        let toolbar_surface = Surface::new(width, TOOLBAR_HEIGHT)
126
+            .context("Failed to create toolbar surface")?;
127
+
128
+        // Set WM_NORMAL_HINTS to lock window size (prevents WM from resizing)
129
+        // Flags: PMinSize (16) | PMaxSize (32) | PSize (8) = 56
130
+        let size_hints: [u32; 18] = [
131
+            56,                      // flags: PSize | PMinSize | PMaxSize
132
+            0, 0,                    // x, y (obsolete)
133
+            window_width, window_height,  // width, height (PSize)
134
+            window_width, window_height,  // min_width, min_height (PMinSize)
135
+            window_width, window_height,  // max_width, max_height (PMaxSize)
136
+            0, 0,                    // width_inc, height_inc
137
+            0, 0,                    // min_aspect_num, min_aspect_den
138
+            0, 0,                    // max_aspect_num, max_aspect_den
139
+            0, 0,                    // base_width, base_height
140
+            0,                       // win_gravity
141
+        ];
142
+        conn.inner().change_property32(
143
+            PropMode::REPLACE,
144
+            window.id(),
145
+            AtomEnum::WM_NORMAL_HINTS,
146
+            AtomEnum::WM_SIZE_HINTS,
147
+            &size_hints,
148
+        )?;
149
+        tracing::debug!("Set WM_NORMAL_HINTS: min/max {}x{}", window_width, window_height);
150
+
151
+        // Tell compositor to bypass this window (reduces effects/lag from picom)
152
+        let bypass_atom = conn.inner()
153
+            .intern_atom(false, b"_NET_WM_BYPASS_COMPOSITOR")?
154
+            .reply()
155
+            .context("Failed to intern bypass atom")?
156
+            .atom;
157
+        conn.inner().change_property32(
158
+            PropMode::REPLACE,
159
+            window.id(),
160
+            bypass_atom,
161
+            AtomEnum::CARDINAL,
162
+            &[1], // 1 = bypass compositor
163
+        )?;
164
+
112
         Ok(Self {
165
         Ok(Self {
113
             conn,
166
             conn,
114
             window,
167
             window,
@@ -118,6 +171,7 @@ impl AnnotationOverlay {
118
             tool,
171
             tool,
119
             history: History::new(),
172
             history: History::new(),
120
             toolbar,
173
             toolbar,
174
+            toolbar_surface,
121
             cursor_manager,
175
             cursor_manager,
122
             needs_redraw: true,
176
             needs_redraw: true,
123
             image_offset_y: TOOLBAR_HEIGHT as i32,
177
             image_offset_y: TOOLBAR_HEIGHT as i32,
@@ -144,18 +198,35 @@ impl AnnotationOverlay {
144
 
198
 
145
         // Event loop
199
         // Event loop
146
         loop {
200
         loop {
147
-            // Wait for event
201
+            // Wait for first event
148
             let event = self.conn.inner().wait_for_event()?;
202
             let event = self.conn.inner().wait_for_event()?;
203
+            let mut needs_redraw = false;
149
 
204
 
150
-            // Translate to InputEvent
205
+            // Translate and handle the first event
151
             if let Some(input_event) = self.translate_event(&event) {
206
             if let Some(input_event) = self.translate_event(&event) {
152
-                let needs_redraw = self.handle_event(input_event)?;
207
+                needs_redraw |= self.handle_event(input_event)?;
208
+            }
153
 
209
 
154
-                if needs_redraw {
210
+            // Check if finished after first event
155
-                    self.redraw()?;
211
+            if self.state.is_finished() {
212
+                break;
213
+            }
214
+
215
+            // Process all pending events before redrawing (batching)
216
+            while let Some(event) = self.conn.inner().poll_for_event()? {
217
+                if let Some(input_event) = self.translate_event(&event) {
218
+                    needs_redraw |= self.handle_event(input_event)?;
219
+                }
220
+                if self.state.is_finished() {
221
+                    break;
156
                 }
222
                 }
157
             }
223
             }
158
 
224
 
225
+            // Single redraw for all batched events
226
+            if needs_redraw {
227
+                self.redraw()?;
228
+            }
229
+
159
             // Check if finished
230
             // Check if finished
160
             if self.state.is_finished() {
231
             if self.state.is_finished() {
161
                 break;
232
                 break;
@@ -491,21 +562,16 @@ impl AnnotationOverlay {
491
         let width = self.canvas.width();
562
         let width = self.canvas.width();
492
         let height = self.canvas.height();
563
         let height = self.canvas.height();
493
 
564
 
494
-        // Create toolbar surface and draw toolbar
565
+        // Draw toolbar to reusable surface
495
-        let toolbar_surface = gartk_render::Surface::new(width, TOOLBAR_HEIGHT)?;
566
+        self.toolbar.draw(&self.toolbar_surface)?;
496
-        self.toolbar.draw(&toolbar_surface)?;
497
 
567
 
498
         // Get toolbar data and convert to BGRA
568
         // Get toolbar data and convert to BGRA
499
-        let mut toolbar_surface = toolbar_surface;
569
+        let toolbar_data = self.toolbar_surface.to_rgba()?;
500
-        let toolbar_data = toolbar_surface.to_rgba()?;
570
+        let toolbar_bgra = rgba_to_bgra(&toolbar_data);
501
-        let mut toolbar_bgra: Vec<u8> = toolbar_data;
502
-        for chunk in toolbar_bgra.chunks_exact_mut(4) {
503
-            chunk.swap(0, 2);
504
-        }
505
 
571
 
506
-        // Blit toolbar at y=0
572
+        // Blit toolbar at y=0 (toolbar is small, no chunking needed)
507
         self.conn.inner().put_image(
573
         self.conn.inner().put_image(
508
-            xproto::ImageFormat::Z_PIXMAP,
574
+            ImageFormat::Z_PIXMAP,
509
             self.window.id(),
575
             self.window.id(),
510
             self.gc,
576
             self.gc,
511
             width as u16,
577
             width as u16,
@@ -520,22 +586,17 @@ impl AnnotationOverlay {
520
         // Get image composite data
586
         // Get image composite data
521
         let surface = self.canvas.composite_surface_mut();
587
         let surface = self.canvas.composite_surface_mut();
522
         let data = surface.to_rgba()?;
588
         let data = surface.to_rgba()?;
523
-        let mut bgra: Vec<u8> = data;
589
+        let bgra = rgba_to_bgra(&data);
524
-        for chunk in bgra.chunks_exact_mut(4) {
525
-            chunk.swap(0, 2);
526
-        }
527
 
590
 
528
-        // Blit image at y=TOOLBAR_HEIGHT
591
+        // Blit image at y=TOOLBAR_HEIGHT using chunked put_image for large images
529
-        self.conn.inner().put_image(
592
+        put_image_chunked(
530
-            xproto::ImageFormat::Z_PIXMAP,
593
+            self.conn.inner(),
531
             self.window.id(),
594
             self.window.id(),
532
             self.gc,
595
             self.gc,
533
             width as u16,
596
             width as u16,
534
             height as u16,
597
             height as u16,
535
             0,
598
             0,
536
             self.image_offset_y as i16,
599
             self.image_offset_y as i16,
537
-            0,
538
-            24,
539
             &bgra,
600
             &bgra,
540
         )?;
601
         )?;
541
 
602
 
@@ -568,3 +629,52 @@ impl AnnotationOverlay {
568
         Ok(())
629
         Ok(())
569
     }
630
     }
570
 }
631
 }
632
+
633
+/// Convert RGBA to BGRA (X11 native format).
634
+fn rgba_to_bgra(data: &[u8]) -> Vec<u8> {
635
+    let mut bgra = data.to_vec();
636
+    for chunk in bgra.chunks_exact_mut(4) {
637
+        chunk.swap(0, 2); // Swap R and B
638
+    }
639
+    bgra
640
+}
641
+
642
+/// Put image data in chunks to avoid exceeding X11 max request size.
643
+fn put_image_chunked<C: X11Connection>(
644
+    conn: &C,
645
+    drawable: u32,
646
+    gc: u32,
647
+    width: u16,
648
+    height: u16,
649
+    dst_x: i16,
650
+    dst_y: i16,
651
+    data: &[u8],
652
+) -> Result<()> {
653
+    let bytes_per_row = width as usize * 4;
654
+    let rows_per_chunk = (MAX_PUT_IMAGE_BYTES / bytes_per_row).max(1);
655
+
656
+    let mut y = 0u16;
657
+    while (y as usize) < height as usize {
658
+        let chunk_height = ((height as usize - y as usize).min(rows_per_chunk)) as u16;
659
+        let start = y as usize * bytes_per_row;
660
+        let end = start + chunk_height as usize * bytes_per_row;
661
+        let chunk_data = &data[start..end];
662
+
663
+        conn.put_image(
664
+            ImageFormat::Z_PIXMAP,
665
+            drawable,
666
+            gc,
667
+            width,
668
+            chunk_height,
669
+            dst_x,
670
+            dst_y + y as i16,
671
+            0,
672
+            24,
673
+            chunk_data,
674
+        )?;
675
+
676
+        y += chunk_height;
677
+    }
678
+
679
+    Ok(())
680
+}