gardesk/garbg / 901b4c8

Browse files

stream-scale animation frames to prevent OOM on large/multi-monitor setups

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
901b4c893f3079951fa3305fae455b925fba2eaa
Parents
76ffb7a
Tree
b45f047

1 changed file

StatusFile+-
M garbg/src/daemon/state.rs 181 33
garbg/src/daemon/state.rsmodified
@@ -1,7 +1,10 @@
1
 //! Daemon state management
1
 //! Daemon state management
2
 
2
 
3
 use anyhow::{Context, Result};
3
 use anyhow::{Context, Result};
4
+use crossbeam_channel::Receiver;
4
 use std::collections::HashMap;
5
 use std::collections::HashMap;
6
+use std::sync::Arc;
7
+use std::thread::JoinHandle;
5
 use std::time::Duration;
8
 use std::time::Duration;
6
 use tokio::net::UnixStream;
9
 use tokio::net::UnixStream;
7
 use tokio::signal::unix::{signal, SignalKind};
10
 use tokio::signal::unix::{signal, SignalKind};
@@ -72,16 +75,36 @@ impl DaemonState {
72
     }
75
     }
73
 }
76
 }
74
 
77
 
78
+/// Frame source strategy for animation playback
79
+enum FrameSource {
80
+    /// All frames pre-scaled in memory (small animations within memory budget)
81
+    Preloaded {
82
+        scaled_frames: Vec<image::RgbaImage>,
83
+        frame_delays: Vec<Duration>,
84
+        current_frame: usize,
85
+    },
86
+    /// Frames scaled on demand by a background producer thread (large animations)
87
+    Streaming {
88
+        /// Receiver for scaled frames from the producer
89
+        rx: Receiver<(image::RgbaImage, Duration)>,
90
+        /// Handle to the producer thread (dropped = detached, thread exits on send error)
91
+        _producer: JoinHandle<()>,
92
+        /// The last successfully received frame (re-displayed if producer is behind)
93
+        last_frame: Option<image::RgbaImage>,
94
+        /// Delay of the last frame
95
+        last_delay: Duration,
96
+        /// Total frame count (for logging)
97
+        #[allow(dead_code)]
98
+        frame_count: usize,
99
+    },
100
+}
101
+
75
 /// Active animation state (works with GIF, WebP, or other animated formats)
102
 /// Active animation state (works with GIF, WebP, or other animated formats)
76
 pub struct ActiveAnimation {
103
 pub struct ActiveAnimation {
77
-    /// Pre-scaled frames for quick rendering
104
+    /// Frame source (preloaded or streaming)
78
-    scaled_frames: Vec<image::RgbaImage>,
105
+    frame_source: FrameSource,
79
-    /// Frame delays (parallel to scaled_frames)
80
-    frame_delays: Vec<Duration>,
81
     /// Animation renderer (double-buffered)
106
     /// Animation renderer (double-buffered)
82
     renderer: AnimationRenderer,
107
     renderer: AnimationRenderer,
83
-    /// Current frame index
84
-    current_frame: usize,
85
     /// Max FPS
108
     /// Max FPS
86
     max_fps: u32,
109
     max_fps: u32,
87
     /// Scale mode (for status)
110
     /// Scale mode (for status)
@@ -93,7 +116,7 @@ pub struct ActiveAnimation {
93
 }
116
 }
94
 
117
 
95
 impl ActiveAnimation {
118
 impl ActiveAnimation {
96
-    /// Create from animation frames (scales in parallel with fast filter)
119
+    /// Create from animation frames, choosing pre-load or streaming based on memory budget
97
     fn from_frames(
120
     fn from_frames(
98
         frames: &[AnimationFrame],
121
         frames: &[AnimationFrame],
99
         renderer: AnimationRenderer,
122
         renderer: AnimationRenderer,
@@ -102,14 +125,52 @@ impl ActiveAnimation {
102
         source: String,
125
         source: String,
103
         screen_width: u32,
126
         screen_width: u32,
104
         screen_height: u32,
127
         screen_height: u32,
128
+        memory_budget: u64,
105
     ) -> Self {
129
     ) -> Self {
106
-        // Scale frames in parallel, limited to available CPU cores
130
+        let per_frame_bytes = screen_width as u64 * screen_height as u64 * 4;
131
+        let total_bytes = per_frame_bytes * frames.len() as u64;
132
+
133
+        let frame_source = if total_bytes <= memory_budget {
134
+            tracing::info!(
135
+                "Animation fits in memory budget ({:.1} MB <= {:.1} MB), pre-scaling all {} frames",
136
+                total_bytes as f64 / (1024.0 * 1024.0),
137
+                memory_budget as f64 / (1024.0 * 1024.0),
138
+                frames.len(),
139
+            );
140
+            Self::preload_frames(frames, screen_width, screen_height, scale_mode)
141
+        } else {
142
+            let max_buffered = (memory_budget / 2 / per_frame_bytes).max(2).min(8) as usize;
143
+            tracing::info!(
144
+                "Animation exceeds memory budget ({:.1} MB > {:.1} MB), streaming with {} frame buffer",
145
+                total_bytes as f64 / (1024.0 * 1024.0),
146
+                memory_budget as f64 / (1024.0 * 1024.0),
147
+                max_buffered,
148
+            );
149
+            Self::stream_frames(frames, screen_width, screen_height, scale_mode, max_buffered)
150
+        };
151
+
152
+        Self {
153
+            frame_source,
154
+            renderer,
155
+            max_fps,
156
+            scale_mode,
157
+            source,
158
+        }
159
+    }
160
+
161
+    /// Pre-scale all frames in parallel (existing behavior, for small animations)
162
+    fn preload_frames(
163
+        frames: &[AnimationFrame],
164
+        screen_width: u32,
165
+        screen_height: u32,
166
+        scale_mode: ScaleMode,
167
+    ) -> FrameSource {
107
         let num_cpus = std::thread::available_parallelism()
168
         let num_cpus = std::thread::available_parallelism()
108
             .map(|n| n.get())
169
             .map(|n| n.get())
109
             .unwrap_or(4);
170
             .unwrap_or(4);
171
+
110
         let scaled_frames: Vec<image::RgbaImage> = std::thread::scope(|s| {
172
         let scaled_frames: Vec<image::RgbaImage> = std::thread::scope(|s| {
111
             let mut results = Vec::with_capacity(frames.len());
173
             let mut results = Vec::with_capacity(frames.len());
112
-            // Process in batches of num_cpus to avoid spawning too many threads
113
             for chunk in frames.chunks(num_cpus) {
174
             for chunk in frames.chunks(num_cpus) {
114
                 let handles: Vec<_> = chunk
175
                 let handles: Vec<_> = chunk
115
                     .iter()
176
                     .iter()
@@ -124,25 +185,68 @@ impl ActiveAnimation {
124
             results
185
             results
125
         });
186
         });
126
 
187
 
127
-        let frame_delays: Vec<Duration> = frames
188
+        let frame_delays: Vec<Duration> = frames.iter().map(|f| f.delay).collect();
128
-            .iter()
129
-            .map(|frame| frame.delay)
130
-            .collect();
131
 
189
 
132
-        Self {
190
+        FrameSource::Preloaded {
133
             scaled_frames,
191
             scaled_frames,
134
             frame_delays,
192
             frame_delays,
135
-            renderer,
136
             current_frame: 0,
193
             current_frame: 0,
137
-            max_fps,
194
+        }
138
-            scale_mode,
195
+    }
139
-            source,
196
+
197
+    /// Set up streaming producer thread for large animations
198
+    fn stream_frames(
199
+        frames: &[AnimationFrame],
200
+        screen_width: u32,
201
+        screen_height: u32,
202
+        scale_mode: ScaleMode,
203
+        buffer_capacity: usize,
204
+    ) -> FrameSource {
205
+        let (tx, rx) = crossbeam_channel::bounded::<(image::RgbaImage, Duration)>(buffer_capacity);
206
+        let source_frames: Arc<Vec<AnimationFrame>> = Arc::new(frames.to_vec());
207
+        let frame_count = frames.len();
208
+
209
+        let producer = std::thread::Builder::new()
210
+            .name("garbg-frame-scaler".into())
211
+            .spawn(move || {
212
+                let mut index = 0usize;
213
+                loop {
214
+                    let frame = &source_frames[index % frame_count];
215
+                    let scaled = scale_image_fast(
216
+                        &frame.image,
217
+                        screen_width,
218
+                        screen_height,
219
+                        scale_mode,
220
+                    );
221
+                    if tx.send((scaled, frame.delay)).is_err() {
222
+                        tracing::debug!("Frame producer exiting: receiver dropped");
223
+                        break;
224
+                    }
225
+                    index += 1;
226
+                }
227
+            })
228
+            .expect("failed to spawn frame scaler thread");
229
+
230
+        // Block on first frame so animation starts with content
231
+        let (first_frame, first_delay) = rx.recv().expect("producer died before first frame");
232
+
233
+        FrameSource::Streaming {
234
+            rx,
235
+            _producer: producer,
236
+            last_frame: Some(first_frame),
237
+            last_delay: first_delay,
238
+            frame_count,
140
         }
239
         }
141
     }
240
     }
142
 
241
 
143
     /// Get the delay for the current frame
242
     /// Get the delay for the current frame
144
     fn current_delay(&self) -> Duration {
243
     fn current_delay(&self) -> Duration {
145
-        let frame_delay = self.frame_delays[self.current_frame];
244
+        let frame_delay = match &self.frame_source {
245
+            FrameSource::Preloaded { frame_delays, current_frame, .. } => {
246
+                frame_delays[*current_frame]
247
+            }
248
+            FrameSource::Streaming { last_delay, .. } => *last_delay,
249
+        };
146
         let min_delay = if self.max_fps > 0 {
250
         let min_delay = if self.max_fps > 0 {
147
             Duration::from_secs_f64(1.0 / self.max_fps as f64)
251
             Duration::from_secs_f64(1.0 / self.max_fps as f64)
148
         } else {
252
         } else {
@@ -153,19 +257,41 @@ impl ActiveAnimation {
153
 
257
 
154
     /// Advance to next frame, returning true if looped
258
     /// Advance to next frame, returning true if looped
155
     fn advance(&mut self) -> bool {
259
     fn advance(&mut self) -> bool {
156
-        self.current_frame += 1;
260
+        match &mut self.frame_source {
157
-        if self.current_frame >= self.scaled_frames.len() {
261
+            FrameSource::Preloaded { scaled_frames, current_frame, .. } => {
158
-            self.current_frame = 0;
262
+                *current_frame += 1;
159
-            true
263
+                if *current_frame >= scaled_frames.len() {
160
-        } else {
264
+                    *current_frame = 0;
161
-            false
265
+                    true
266
+                } else {
267
+                    false
268
+                }
269
+            }
270
+            FrameSource::Streaming { rx, last_frame, last_delay, .. } => {
271
+                match rx.try_recv() {
272
+                    Ok((frame, delay)) => {
273
+                        *last_frame = Some(frame);
274
+                        *last_delay = delay;
275
+                    }
276
+                    Err(crossbeam_channel::TryRecvError::Empty) => {
277
+                        tracing::trace!("Frame producer behind, re-displaying last frame");
278
+                    }
279
+                    Err(crossbeam_channel::TryRecvError::Disconnected) => {
280
+                        tracing::warn!("Frame producer disconnected");
281
+                    }
282
+                }
283
+                false
284
+            }
162
         }
285
         }
163
     }
286
     }
164
 
287
 
165
     /// Get frame count
288
     /// Get frame count
166
     #[allow(dead_code)]
289
     #[allow(dead_code)]
167
     fn frame_count(&self) -> usize {
290
     fn frame_count(&self) -> usize {
168
-        self.scaled_frames.len()
291
+        match &self.frame_source {
292
+            FrameSource::Preloaded { scaled_frames, .. } => scaled_frames.len(),
293
+            FrameSource::Streaming { frame_count, .. } => *frame_count,
294
+        }
169
     }
295
     }
170
 }
296
 }
171
 
297
 
@@ -609,9 +735,17 @@ impl Daemon {
609
         let anim = self.animation.as_mut()
735
         let anim = self.animation.as_mut()
610
             .ok_or_else(|| anyhow::anyhow!("No active animation"))?;
736
             .ok_or_else(|| anyhow::anyhow!("No active animation"))?;
611
 
737
 
612
-        // Render current frame
738
+        // Render current frame (inline match for split borrowing of frame_source vs renderer)
613
-        let frame = &anim.scaled_frames[anim.current_frame];
739
+        match &anim.frame_source {
614
-        anim.renderer.render_and_present(conn, frame)?;
740
+            FrameSource::Preloaded { scaled_frames, current_frame, .. } => {
741
+                anim.renderer.render_and_present(conn, &scaled_frames[*current_frame])?;
742
+            }
743
+            FrameSource::Streaming { last_frame, .. } => {
744
+                if let Some(frame) = last_frame {
745
+                    anim.renderer.render_and_present(conn, frame)?;
746
+                }
747
+            }
748
+        }
615
 
749
 
616
         // Advance to next frame
750
         // Advance to next frame
617
         anim.advance();
751
         anim.advance();
@@ -995,13 +1129,19 @@ impl Daemon {
995
         let renderer = AnimationRenderer::new(conn)?;
1129
         let renderer = AnimationRenderer::new(conn)?;
996
         let (width, height) = conn.screen_dimensions();
1130
         let (width, height) = conn.screen_dimensions();
997
 
1131
 
1132
+        let per_frame_mb = (width as f64 * height as f64 * 4.0) / (1024.0 * 1024.0);
998
         tracing::info!(
1133
         tracing::info!(
999
-            "Animation loaded: {} frames, {:.1} FPS ({})",
1134
+            "Animation loaded: {} frames, {:.1} FPS ({}), screen {}x{}, {:.1} MB/frame, {:.1} MB total",
1000
             frame_count,
1135
             frame_count,
1001
             avg_fps,
1136
             avg_fps,
1002
-            format_name
1137
+            format_name,
1138
+            width,
1139
+            height,
1140
+            per_frame_mb,
1141
+            per_frame_mb * frame_count as f64,
1003
         );
1142
         );
1004
 
1143
 
1144
+        let memory_budget = self.state.config.animation.memory_budget_mb * 1024 * 1024;
1005
         self.animation = Some(ActiveAnimation::from_frames(
1145
         self.animation = Some(ActiveAnimation::from_frames(
1006
             &frames,
1146
             &frames,
1007
             renderer,
1147
             renderer,
@@ -1010,6 +1150,7 @@ impl Daemon {
1010
             source.to_string(),
1150
             source.to_string(),
1011
             width as u32,
1151
             width as u32,
1012
             height as u32,
1152
             height as u32,
1153
+            memory_budget,
1013
         ));
1154
         ));
1014
 
1155
 
1015
         Ok(())
1156
         Ok(())
@@ -1067,12 +1208,18 @@ impl Daemon {
1067
         let renderer = AnimationRenderer::new(conn)?;
1208
         let renderer = AnimationRenderer::new(conn)?;
1068
         let (width, height) = conn.screen_dimensions();
1209
         let (width, height) = conn.screen_dimensions();
1069
 
1210
 
1211
+        let per_frame_mb = (width as f64 * height as f64 * 4.0) / (1024.0 * 1024.0);
1070
         tracing::info!(
1212
         tracing::info!(
1071
-            "Video loaded: {} frames, {:.1} FPS",
1213
+            "Video loaded: {} frames, {:.1} FPS, screen {}x{}, {:.1} MB/frame, {:.1} MB total",
1072
             frame_count,
1214
             frame_count,
1073
-            avg_fps
1215
+            avg_fps,
1216
+            width,
1217
+            height,
1218
+            per_frame_mb,
1219
+            per_frame_mb * frame_count as f64,
1074
         );
1220
         );
1075
 
1221
 
1222
+        let memory_budget = self.state.config.animation.memory_budget_mb * 1024 * 1024;
1076
         self.animation = Some(ActiveAnimation::from_frames(
1223
         self.animation = Some(ActiveAnimation::from_frames(
1077
             &frames,
1224
             &frames,
1078
             renderer,
1225
             renderer,
@@ -1081,6 +1228,7 @@ impl Daemon {
1081
             source.to_string(),
1228
             source.to_string(),
1082
             width as u32,
1229
             width as u32,
1083
             height as u32,
1230
             height as u32,
1231
+            memory_budget,
1084
         ));
1232
         ));
1085
 
1233
 
1086
         Ok(())
1234
         Ok(())