@@ -11,6 +11,9 @@ use crate::config::ScaleMode; |
| 11 | use crate::media::{scale_image_fast, AnimatedGif}; | 11 | use crate::media::{scale_image_fast, AnimatedGif}; |
| 12 | use crate::x11::{AnimationRenderer, Connection}; | 12 | use crate::x11::{AnimationRenderer, Connection}; |
| 13 | | 13 | |
| | 14 | +/// Default memory budget for pre-scaled animation frames (256 MB) |
| | 15 | +const DEFAULT_MEMORY_BUDGET: u64 = 256 * 1024 * 1024; |
| | 16 | + |
| 14 | /// Configuration for the animation loop | 17 | /// Configuration for the animation loop |
| 15 | #[derive(Debug, Clone)] | 18 | #[derive(Debug, Clone)] |
| 16 | pub struct AnimationConfig { | 19 | pub struct AnimationConfig { |
@@ -38,8 +41,13 @@ pub struct AnimationLoop { |
| 38 | gif: AnimatedGif, | 41 | gif: AnimatedGif, |
| 39 | /// X11 animation renderer | 42 | /// X11 animation renderer |
| 40 | renderer: AnimationRenderer, | 43 | renderer: AnimationRenderer, |
| 41 | - /// Pre-scaled frames (cached) | 44 | + /// Pre-scaled frames (empty if streaming mode) |
| 42 | scaled_frames: Vec<image::RgbaImage>, | 45 | scaled_frames: Vec<image::RgbaImage>, |
| | 46 | + /// Whether to scale frames on-the-fly instead of pre-scaling |
| | 47 | + streaming: bool, |
| | 48 | + /// Screen dimensions (used for on-the-fly scaling) |
| | 49 | + screen_width: u32, |
| | 50 | + screen_height: u32, |
| 43 | /// Configuration | 51 | /// Configuration |
| 44 | config: AnimationConfig, | 52 | config: AnimationConfig, |
| 45 | /// Whether the animation is paused | 53 | /// Whether the animation is paused |
@@ -57,25 +65,42 @@ impl AnimationLoop { |
| 57 | ) -> Result<Self> { | 65 | ) -> Result<Self> { |
| 58 | let renderer = AnimationRenderer::new(conn)?; | 66 | let renderer = AnimationRenderer::new(conn)?; |
| 59 | let (screen_width, screen_height) = renderer.dimensions(); | 67 | let (screen_width, screen_height) = renderer.dimensions(); |
| 60 | - | 68 | + let sw = screen_width as u32; |
| 61 | - // Pre-scale all frames | 69 | + let sh = screen_height as u32; |
| 62 | - let scaled_frames: Vec<image::RgbaImage> = gif | 70 | + |
| 63 | - .frames() | 71 | + let per_frame_bytes = sw as u64 * sh as u64 * 4; |
| 64 | - .iter() | 72 | + let total_bytes = per_frame_bytes * gif.frame_count() as u64; |
| 65 | - .map(|frame| { | 73 | + |
| 66 | - scale_image_fast( | 74 | + let (scaled_frames, streaming) = if total_bytes <= DEFAULT_MEMORY_BUDGET { |
| 67 | - &frame.image, | 75 | + tracing::info!( |
| 68 | - screen_width as u32, | 76 | + "Animation fits in memory budget ({:.1} MB), pre-scaling all {} frames", |
| 69 | - screen_height as u32, | 77 | + total_bytes as f64 / (1024.0 * 1024.0), |
| 70 | - config.scale_mode, | 78 | + gif.frame_count(), |
| 71 | - ) | 79 | + ); |
| 72 | - }) | 80 | + let frames: Vec<image::RgbaImage> = gif |
| 73 | - .collect(); | 81 | + .frames() |
| | 82 | + .iter() |
| | 83 | + .map(|frame| { |
| | 84 | + scale_image_fast(&frame.image, sw, sh, config.scale_mode) |
| | 85 | + }) |
| | 86 | + .collect(); |
| | 87 | + (frames, false) |
| | 88 | + } else { |
| | 89 | + tracing::info!( |
| | 90 | + "Animation exceeds memory budget ({:.1} MB > {:.1} MB), scaling frames on-the-fly", |
| | 91 | + total_bytes as f64 / (1024.0 * 1024.0), |
| | 92 | + DEFAULT_MEMORY_BUDGET as f64 / (1024.0 * 1024.0), |
| | 93 | + ); |
| | 94 | + (Vec::new(), true) |
| | 95 | + }; |
| 74 | | 96 | |
| 75 | Ok(Self { | 97 | Ok(Self { |
| 76 | gif, | 98 | gif, |
| 77 | renderer, | 99 | renderer, |
| 78 | scaled_frames, | 100 | scaled_frames, |
| | 101 | + streaming, |
| | 102 | + screen_width: sw, |
| | 103 | + screen_height: sh, |
| 79 | config, | 104 | config, |
| 80 | paused: Arc::new(AtomicBool::new(false)), | 105 | paused: Arc::new(AtomicBool::new(false)), |
| 81 | stop: Arc::new(AtomicBool::new(false)), | 106 | stop: Arc::new(AtomicBool::new(false)), |
@@ -165,8 +190,18 @@ impl AnimationLoop { |
| 165 | } | 190 | } |
| 166 | | 191 | |
| 167 | // Render current frame | 192 | // Render current frame |
| 168 | - let scaled_frame = &self.scaled_frames[frame_index]; | 193 | + if self.streaming { |
| 169 | - self.renderer.render_and_present(conn, scaled_frame)?; | 194 | + let scaled = scale_image_fast( |
| | 195 | + &self.gif.frames()[frame_index].image, |
| | 196 | + self.screen_width, |
| | 197 | + self.screen_height, |
| | 198 | + self.config.scale_mode, |
| | 199 | + ); |
| | 200 | + self.renderer.render_and_present(conn, &scaled)?; |
| | 201 | + } else { |
| | 202 | + let scaled_frame = &self.scaled_frames[frame_index]; |
| | 203 | + self.renderer.render_and_present(conn, scaled_frame)?; |
| | 204 | + } |
| 170 | | 205 | |
| 171 | // Get delay for current frame | 206 | // Get delay for current frame |
| 172 | let frame_delay = self.gif.frames()[frame_index].delay; | 207 | let frame_delay = self.gif.frames()[frame_index].delay; |
@@ -199,10 +234,20 @@ impl AnimationLoop { |
| 199 | } | 234 | } |
| 200 | | 235 | |
| 201 | let frame_index = self.gif.current_index(); | 236 | let frame_index = self.gif.current_index(); |
| 202 | - let scaled_frame = &self.scaled_frames[frame_index]; | | |
| 203 | | 237 | |
| 204 | // Render current frame | 238 | // Render current frame |
| 205 | - self.renderer.render_and_present(conn, scaled_frame)?; | 239 | + if self.streaming { |
| | 240 | + let scaled = scale_image_fast( |
| | 241 | + &self.gif.frames()[frame_index].image, |
| | 242 | + self.screen_width, |
| | 243 | + self.screen_height, |
| | 244 | + self.config.scale_mode, |
| | 245 | + ); |
| | 246 | + self.renderer.render_and_present(conn, &scaled)?; |
| | 247 | + } else { |
| | 248 | + let scaled_frame = &self.scaled_frames[frame_index]; |
| | 249 | + self.renderer.render_and_present(conn, scaled_frame)?; |
| | 250 | + } |
| 206 | | 251 | |
| 207 | // Get delay for current frame | 252 | // Get delay for current frame |
| 208 | let delay = self.gif.current_frame().delay; | 253 | let delay = self.gif.current_frame().delay; |