cosmic_files/
large_image.rs

1use cosmic::widget;
2use image::ImageReader;
3use std::collections::{HashMap, HashSet};
4use std::path::{Path, PathBuf};
5
6/// Bytes per pixel in RGBA format (Red, Green, Blue, Alpha = 4 bytes)
7pub const RGBA_BYTES_PER_PIXEL: u64 = 4;
8
9/// System memory reserve in MB to maintain for system stability (prevents thrashing)
10/// Note: RAM checking is currently only available on Linux via procfs.
11/// On Windows and macOS, only GPU buffer limits are enforced.
12#[allow(dead_code)]
13const SYSTEM_MEMORY_RESERVE_MB: u64 = 500;
14
15/// Maximum memory allocation for gallery image decoding in MB.
16/// Gallery mode uses the full memory budget since only one image decodes at a time.
17/// This matches the ThumbCfg max_mem_mb budget for consistency.
18const GALLERY_MEMORY_LIMIT_MB: u64 = 2000;
19
20/// Threshold for considering an image "large" requiring GPU tiling
21/// Atlas fragment/tile size in pixels. Large images are split into fragments of this size.
22/// Must match the atlas SIZE constant in libcosmic/iced/wgpu/src/image/atlas.rs
23pub const ATLAS_FRAGMENT_SIZE: u32 = 4096;
24
25/// Conversion factor: 1 MB = 1024 * 1024 bytes (binary megabyte, used for RAM calculations)
26pub const MB_TO_BYTES: u64 = 1024 * 1024;
27
28/// Conversion factor: 1 MB = 1000 * 1000 bytes (decimal megabyte, used by image crate)
29/// The image crate's memory limits use decimal MB, not binary MB.
30pub const DECIMAL_MB_TO_BYTES: u64 = 1000 * 1000;
31
32/// Scale factor for HiDPI displays - decode at higher resolution than display size
33/// for better quality on high-DPI screens. 1.5x provides good balance between
34/// quality and memory usage and also prevets re-decoding on small windows size adjustments.
35const DISPLAY_SCALE_FACTOR: f32 = 1.5;
36
37/// Calculate optimal target dimensions for decoding based on display size.
38/// Returns None if no resizing is needed (image is smaller than display).
39///
40/// This helps reduce memory usage by decoding large images at a resolution
41/// appropriate for the display, rather than always using full resolution.
42pub fn calculate_target_dimensions(
43    image_width: u32,
44    image_height: u32,
45    display_width: u32,
46    display_height: u32,
47) -> Option<(u32, u32)> {
48    let target_width = (display_width as f32 * DISPLAY_SCALE_FACTOR) as u32;
49    let target_height = (display_height as f32 * DISPLAY_SCALE_FACTOR) as u32;
50
51    if image_width <= target_width && image_height <= target_height {
52        return None;
53    }
54
55    let image_aspect = image_width as f32 / image_height as f32;
56    let target_aspect = target_width as f32 / target_height as f32;
57
58    let (new_width, new_height) = if image_aspect > target_aspect {
59        let w = target_width;
60        let h = (target_width as f32 / image_aspect) as u32;
61        (w, h)
62    } else {
63        let h = target_height;
64        let w = (target_height as f32 * image_aspect) as u32;
65        (w, h)
66    };
67
68    let new_width = new_width.max(1);
69    let new_height = new_height.max(1);
70
71    log::info!(
72        "Calculated target dimensions: {}x{} -> {}x{} (display: {}x{}, scale: {}x)",
73        image_width,
74        image_height,
75        new_width,
76        new_height,
77        display_width,
78        display_height,
79        DISPLAY_SCALE_FACTOR
80    );
81
82    Some((new_width, new_height))
83}
84
85/// Check if an image's dimensions would exceed the available memory budget.
86/// Returns true if the image is too large to decode.
87pub fn exceeds_memory_limit(width: u32, height: u32, memory_limit_mb: u64) -> bool {
88    let Some(bytes_needed) = calculate_image_memory(width, height) else {
89        // Overflow in calculation means it definitely exceeds any reasonable limit
90        return true;
91    };
92
93    let max_bytes = memory_limit_mb * MB_TO_BYTES;
94    bytes_needed > max_bytes
95}
96
97/// Check if an image should use GPU tiling for display.
98/// Images larger than the atlas fragment size need to be split into tiles for GPU upload.
99pub fn should_use_tiling(width: u32, height: u32) -> bool {
100    width > ATLAS_FRAGMENT_SIZE || height > ATLAS_FRAGMENT_SIZE
101}
102
103/// Determine if an image should use the dedicated worker for thumbnail generation.
104/// Returns (use_dedicated_worker, effective_max_mb, effective_jobs).
105///
106/// Large images that exceed per-worker memory budget get routed to a dedicated worker
107/// with full memory budget. Smaller images use the normal parallel worker pool.
108pub fn should_use_dedicated_worker(
109    width: u32,
110    height: u32,
111    total_budget_mb: u64,
112    parallel_workers: usize,
113) -> (bool, u64, usize) {
114    if width == 0 || height == 0 {
115        log::warn!(
116            "Invalid image dimensions {}x{}, using normal queue",
117            width,
118            height
119        );
120        return (false, total_budget_mb, parallel_workers);
121    }
122
123    let Some(bytes_needed) = calculate_image_memory(width, height) else {
124        log::warn!(
125            "Image dimensions {}x{} overflow memory calculation, using normal queue",
126            width,
127            height
128        );
129        return (false, total_budget_mb, parallel_workers);
130    };
131
132    let mb_needed = bytes_needed / MB_TO_BYTES;
133    let per_worker_budget_mb = total_budget_mb / parallel_workers as u64;
134
135    if mb_needed > per_worker_budget_mb {
136        log::info!(
137            "Large image {}x{} needs {}MB (exceeds per-worker {}MB), using dedicated worker",
138            width,
139            height,
140            mb_needed,
141            per_worker_budget_mb
142        );
143        // Use dedicated worker with full budget
144        (true, total_budget_mb, 1)
145    } else {
146        log::debug!(
147            "Normal image {}x{} needs {}MB (within per-worker {}MB), using parallel workers",
148            width,
149            height,
150            mb_needed,
151            per_worker_budget_mb
152        );
153        // Use parallel worker pool with shared budget
154        (false, total_budget_mb, parallel_workers)
155    }
156}
157
158/// Get the dimensions of an image without fully decoding it
159pub fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> {
160    match ImageReader::open(path) {
161        Ok(reader) => match reader.into_dimensions() {
162            Ok((width, height)) => {
163                log::debug!(
164                    "Image dimensions: {}x{} for {}",
165                    width,
166                    height,
167                    path.display()
168                );
169                Some((width, height))
170            }
171            Err(e) => {
172                log::warn!("Failed to get dimensions for {}: {}", path.display(), e);
173                None
174            }
175        },
176        Err(e) => {
177            log::warn!("Failed to open image reader for {}: {}", path.display(), e);
178            None
179        }
180    }
181}
182
183/// Calculate the memory required to decode an image in bytes.
184/// Returns None if the calculation overflows.
185fn calculate_image_memory(width: u32, height: u32) -> Option<u64> {
186    let pixels = (width as u64).checked_mul(height as u64)?;
187    pixels.checked_mul(RGBA_BYTES_PER_PIXEL)
188}
189
190/// Check if there's sufficient system RAM to decode an image (Linux only).
191/// Returns: (has_memory, error_message)
192#[cfg(target_os = "linux")]
193fn check_ram_available(width: u32, height: u32) -> (bool, Option<String>) {
194    use procfs::Current;
195
196    let Some(bytes_needed) = calculate_image_memory(width, height) else {
197        let error_msg = format!(
198            "Image dimensions too large: {}x{} causes overflow in memory calculation",
199            width, height
200        );
201        log::error!("{}", error_msg);
202        return (false, Some(error_msg));
203    };
204
205    let mb_needed = bytes_needed / MB_TO_BYTES;
206
207    match procfs::Meminfo::current() {
208        Ok(meminfo) => {
209            // MemAvailable includes reclaimable cache and is the best estimate of
210            // actually available memory for new allocations
211            let available_kb = meminfo.mem_available.unwrap_or(0);
212            let available_bytes = available_kb * 1024;
213
214            // Maintain system reserve to prevent thrashing and OOM killer
215            let min_reserve_bytes = SYSTEM_MEMORY_RESERVE_MB * MB_TO_BYTES;
216            let usable_bytes = available_bytes.saturating_sub(min_reserve_bytes);
217
218            if bytes_needed > usable_bytes {
219                let available_mb = available_bytes / MB_TO_BYTES;
220                let error_msg = format!(
221                    "Insufficient memory: need {}MB, available {}MB. Try closing other applications.",
222                    mb_needed, available_mb
223                );
224                log::warn!("{}", error_msg);
225                return (false, Some(error_msg));
226            }
227
228            (true, None)
229        }
230        Err(e) => {
231            log::warn!("Failed to read /proc/meminfo: {}. Skipping RAM check.", e);
232            // Graceful fallback: assume RAM is available
233            (true, None)
234        }
235    }
236}
237
238#[cfg(not(target_os = "linux"))]
239fn check_ram_available(_width: u32, _height: u32) -> (bool, Option<String>) {
240    // RAM checking not implemented for this platform
241    (true, None)
242}
243
244pub fn check_memory_available(width: u32, height: u32) -> (bool, Option<String>) {
245    if width == 0 || height == 0 {
246        let error_msg = format!(
247            "Invalid image dimensions: {}x{} (zero dimension)",
248            width, height
249        );
250        log::error!("{}", error_msg);
251        return (false, Some(error_msg));
252    }
253
254    // Check system RAM availability
255    check_ram_available(width, height)
256}
257
258/// Decode a large image asynchronously in a blocking thread pool.
259///
260/// This function is used for gallery mode where full-resolution images need to be loaded.
261/// It uses the full memory budget (GALLERY_MEMORY_LIMIT_MB) since only one image
262/// decodes at a time in gallery mode.
263pub async fn decode_large_image(
264    path: PathBuf,
265    target_dimensions: Option<(u32, u32)>,
266) -> Option<(PathBuf, u32, u32, Vec<u8>)> {
267    // Decode image in blocking thread pool (CPU-intensive work should not block)
268    tokio::task::spawn_blocking(move || {
269        log::info!("Starting async decode of {}", path.display());
270
271        // Use ImageReader with explicit memory limits to avoid "Memory limit exceeded" errors
272        // Gallery mode uses the full memory budget since only one image decodes at a time
273        match image::ImageReader::open(&path) {
274            Ok(reader) => {
275                match reader.with_guessed_format() {
276                    Ok(mut reader) => {
277                        // Note: image crate uses decimal MB (1000^2), not binary MB (1024^2)
278                        let mut limits = image::Limits::default();
279                        limits.max_alloc = Some(GALLERY_MEMORY_LIMIT_MB * DECIMAL_MB_TO_BYTES);
280                        reader.limits(limits);
281
282                        match reader.decode() {
283                            Ok(img) => {
284                                let rgba = img.into_rgba8();
285                                let orig_width = rgba.width();
286                                let orig_height = rgba.height();
287
288                                // Resize if target dimensions provided
289                                let (final_img, width, height) = if let Some((target_w, target_h)) = target_dimensions {
290                                    log::info!(
291                                        "Resizing {}x{} -> {}x{} for memory optimization: {}",
292                                        orig_width, orig_height, target_w, target_h,
293                                        path.display()
294                                    );
295
296                                    // Use Lanczos3 for high-quality downsampling
297                                    let resized = image::imageops::resize(
298                                        &rgba,
299                                        target_w,
300                                        target_h,
301                                        image::imageops::FilterType::Lanczos3,
302                                    );
303
304                                    let resized_w = resized.width();
305                                    let resized_h = resized.height();
306
307                                    log::info!(
308                                        "Resize complete: {}x{} image now uses ~{} MB instead of ~{} MB",
309                                        resized_w, resized_h,
310                                        (resized_w as u64 * resized_h as u64 * 4) / MB_TO_BYTES,
311                                        (orig_width as u64 * orig_height as u64 * 4) / MB_TO_BYTES
312                                    );
313
314                                    (resized, resized_w, resized_h)
315                                } else {
316                                    log::info!(
317                                        "Decoded {}x{} image at full resolution: {}",
318                                        orig_width, orig_height,
319                                        path.display()
320                                    );
321                                    (rgba, orig_width, orig_height)
322                                };
323
324                                let pixels = final_img.into_raw();
325                                Some((path, width, height, pixels))
326                            }
327                            Err(e) => {
328                                log::warn!("Failed to decode {}: {}", path.display(), e);
329                                None
330                            }
331                        }
332                    }
333                    Err(e) => {
334                        log::warn!("Failed to guess format for {}: {}", path.display(), e);
335                        None
336                    }
337                }
338            }
339            Err(e) => {
340                log::warn!("Failed to open {}: {}", path.display(), e);
341                None
342            }
343        }
344    })
345    .await
346    .ok()
347    .flatten()
348}
349
350/// Manages state and operations for large image decoding in gallery mode
351#[derive(Debug, Default)]
352pub struct LargeImageManager {
353    /// Paths of images currently being decoded
354    decoding_images: HashSet<PathBuf>,
355    /// Cache of decoded image handles
356    decoded_images: HashMap<PathBuf, widget::image::Handle>,
357    /// Display dimensions used for each decoded image (for resize detection)
358    decoded_display_sizes: HashMap<PathBuf, (u32, u32)>,
359    /// Errors encountered during decoding
360    decode_errors: HashMap<PathBuf, String>,
361    /// Generation counter for each decode to support cancellation.
362    /// When a new decode is started for the same path, the generation is incremented.
363    /// Only decodes matching the current generation are accepted when they complete.
364    decode_generations: HashMap<PathBuf, u64>,
365}
366
367impl LargeImageManager {
368    pub fn new() -> Self {
369        Self::default()
370    }
371
372    pub fn is_decoding(&self, path: &Path) -> bool {
373        self.decoding_images.contains(path)
374    }
375
376    pub fn get_decoded(&self, path: &Path) -> Option<&widget::image::Handle> {
377        self.decoded_images.get(path)
378    }
379
380    pub fn get_error(&self, path: &Path) -> Option<&String> {
381        self.decode_errors.get(path)
382    }
383
384    /// Store a decoded image if the generation matches (not superseded by newer decode).
385    /// Returns true if stored, false if rejected due to generation mismatch.
386    pub fn store_decoded_with_generation(
387        &mut self,
388        path: PathBuf,
389        handle: widget::image::Handle,
390        display_size: Option<(u32, u32)>,
391        generation: u64,
392    ) -> bool {
393        // Check if this decode is still current (not superseded by a newer one)
394        if let Some(&current_gen) = self.decode_generations.get(&path)
395            && generation != current_gen
396        {
397            log::info!(
398                "Discarding outdated decode for {} (generation {} != current {})",
399                path.display(),
400                generation,
401                current_gen
402            );
403            return false;
404        }
405
406        log::info!(
407            "Storing decoded image for {} (generation {})",
408            path.display(),
409            generation
410        );
411
412        self.decoded_images.insert(path.clone(), handle);
413        if let Some(size) = display_size {
414            self.decoded_display_sizes.insert(path.clone(), size);
415        }
416        self.decoding_images.remove(&path);
417        true
418    }
419
420    pub fn store_error(&mut self, path: PathBuf, error: String) {
421        self.decode_errors.insert(path.clone(), error);
422        self.decoding_images.remove(&path);
423    }
424
425    pub fn clear_error(&mut self, path: &Path) {
426        self.decode_errors.remove(path);
427    }
428
429    pub fn clear_cache(&mut self) {
430        log::info!(
431            "Clearing {} cached images from large image manager",
432            self.decoded_images.len()
433        );
434        self.decoded_images.clear();
435    }
436
437    pub fn cache_size(&self) -> usize {
438        self.decoded_images.len()
439    }
440
441    pub fn cache_is_empty(&self) -> bool {
442        self.decoded_images.is_empty()
443    }
444
445    /// Check if an image should be re-decoded due to display size increase.
446    /// Returns true only if the display size has INCREASED by more than 20% in either dimension.
447    /// Does NOT re-decode for smaller sizes (GPU can efficiently downscale).
448    pub fn needs_redecode_for_size(
449        &self,
450        path: &Path,
451        new_display_size: Option<(u32, u32)>,
452    ) -> bool {
453        let Some(new_size) = new_display_size else {
454            return false;
455        };
456
457        let Some(&old_size) = self.decoded_display_sizes.get(path) else {
458            return false;
459        };
460
461        const REDECODE_THRESHOLD: f32 = 0.2;
462
463        let width_increase = (new_size.0 as f32 / old_size.0 as f32) - 1.0;
464        let height_increase = (new_size.1 as f32 / old_size.1 as f32) - 1.0;
465
466        let needs_redecode =
467            width_increase > REDECODE_THRESHOLD || height_increase > REDECODE_THRESHOLD;
468
469        if needs_redecode {
470            log::info!(
471                "Display size increased significantly for {}: {}x{} -> {}x{} (increase: {:.1}% width, {:.1}% height) - re-decoding at higher resolution",
472                path.display(),
473                old_size.0,
474                old_size.1,
475                new_size.0,
476                new_size.1,
477                width_increase * 100.0,
478                height_increase * 100.0
479            );
480        } else if width_increase < -REDECODE_THRESHOLD || height_increase < -REDECODE_THRESHOLD {
481            log::debug!(
482                "Display size decreased for {}: {}x{} -> {}x{} (decrease: {:.1}% width, {:.1}% height) - keeping existing higher resolution",
483                path.display(),
484                old_size.0,
485                old_size.1,
486                new_size.0,
487                new_size.1,
488                width_increase * 100.0,
489                height_increase * 100.0
490            );
491        }
492
493        needs_redecode
494    }
495
496    /// Attempt to decode a large image, checking memory availability first.
497    /// Returns (should_decode, target_dimensions, generation) tuple.
498    pub fn try_decode(
499        &mut self,
500        path: &PathBuf,
501        display_dimensions: Option<(u32, u32)>,
502    ) -> (bool, Option<(u32, u32)>, u64) {
503        self.clear_error(path);
504        let is_currently_decoding = self.is_decoding(path);
505        let needs_redecode = self.needs_redecode_for_size(path, display_dimensions);
506
507        if is_currently_decoding && !needs_redecode {
508            // Get current generation for the ongoing decode
509            let generation = self.decode_generations.get(path).copied().unwrap_or(0);
510            return (false, None, generation);
511        }
512
513        if self.get_decoded(path).is_some() && !needs_redecode && !is_currently_decoding {
514            let generation = self.decode_generations.get(path).copied().unwrap_or(0);
515            return (false, None, generation);
516        }
517
518        let Some((width, height)) = get_image_dimensions(path) else {
519            self.store_error(path.clone(), "Failed to read image dimensions".to_string());
520            return (false, None, 0);
521        };
522
523        let target_dimensions = if let Some((display_w, display_h)) = display_dimensions {
524            calculate_target_dimensions(width, height, display_w, display_h)
525        } else {
526            None
527        };
528
529        // Check memory for target size (if resizing) or full size
530        let (check_w, check_h) = target_dimensions.unwrap_or((width, height));
531        if !self.ensure_memory_available(path, check_w, check_h) {
532            return (false, None, 0);
533        }
534
535        // Increment generation counter (cancels any previous decode)
536        let generation = self
537            .decode_generations
538            .entry(path.clone())
539            .and_modify(|g| *g += 1)
540            .or_insert(1);
541        let generation = *generation;
542
543        if is_currently_decoding {
544            log::info!(
545                "Cancelling previous decode for {} and starting new one (generation {})",
546                path.display(),
547                generation
548            );
549        }
550
551        // Mark as decoding
552        self.decoding_images.insert(path.clone());
553        (true, target_dimensions, generation)
554    }
555
556    /// Check if sufficient memory is available, clearing cache if needed.
557    /// Returns true if memory is available, false otherwise.
558    fn ensure_memory_available(&mut self, path: &Path, width: u32, height: u32) -> bool {
559        let (has_memory, error_opt) = check_memory_available(width, height);
560
561        if has_memory {
562            return true;
563        }
564
565        if self.cache_is_empty() {
566            if let Some(error_msg) = error_opt {
567                self.store_error(path.to_path_buf(), error_msg);
568                log::warn!(
569                    "Cannot load {}: insufficient memory and cache is empty",
570                    path.display()
571                );
572            }
573            return false;
574        }
575
576        log::info!(
577            "Insufficient memory, clearing {} cached images",
578            self.cache_size()
579        );
580        self.clear_cache();
581
582        let (has_memory_after_clear, error_opt_after) = check_memory_available(width, height);
583
584        if has_memory_after_clear {
585            log::info!("Memory available after cache clear, proceeding with decode");
586            return true;
587        }
588
589        if let Some(error_msg) = error_opt_after {
590            self.store_error(path.to_path_buf(), error_msg);
591            log::warn!(
592                "Cannot load {}: insufficient memory even after cache clear",
593                path.display()
594            );
595        }
596        false
597    }
598}