From 0353009321114bbd3a07e696fb3e6a12ed13e1ae Mon Sep 17 00:00:00 2001 From: Frederic Laing Date: Sun, 16 Nov 2025 18:12:51 +0100 Subject: [PATCH] move large image handling out of tab and into new module large_image --- src/large_image.rs | 320 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/tab.rs | 315 +++++--------------------------------------- 3 files changed, 355 insertions(+), 281 deletions(-) create mode 100644 src/large_image.rs diff --git a/src/large_image.rs b/src/large_image.rs new file mode 100644 index 0000000..7510348 --- /dev/null +++ b/src/large_image.rs @@ -0,0 +1,320 @@ +use cosmic::widget; +use image::ImageReader; +use std::{ + collections::{HashMap, HashSet}, + path::{Path, PathBuf}, +}; + +/// Bytes per pixel in RGBA format (Red, Green, Blue, Alpha = 4 bytes) +pub const RGBA_BYTES_PER_PIXEL: u64 = 4; + +/// Overhead factor for image decoding operations (30% additional memory for decode buffers, +/// fragment allocations, and intermediate representations during image decoding) +const DECODE_OVERHEAD_FACTOR: f64 = 1.3; + +/// System memory reserve in MB to maintain for system stability (prevents thrashing) +/// Note: RAM checking is currently only available on Linux via procfs. +/// On Windows and macOS, only GPU buffer limits are enforced. +const SYSTEM_MEMORY_RESERVE_MB: u64 = 500; + +/// Maximum memory allocation for gallery image decoding in MB. +/// Gallery mode uses the full memory budget since only one image decodes at a time. +/// This matches the ThumbCfg max_mem_mb budget for consistency. +const GALLERY_MEMORY_LIMIT_MB: u64 = 2000; + +/// Threshold for considering an image "large" requiring GPU tiling +/// Atlas fragment/tile size in pixels. Large images are split into fragments of this size. +/// Must match the atlas SIZE constant in libcosmic/iced/wgpu/src/image/atlas.rs +pub const ATLAS_FRAGMENT_SIZE: u32 = 4096; + +/// Conservative GPU buffer size limit in MB. Each atlas fragment can be up to this size. +/// Based on wgpu device limits - most GPUs support at least 256MB buffers. +/// Reference: https://docs.rs/wgpu/latest/wgpu/struct.Limits.html#structfield.max_buffer_size +const MAX_GPU_BUFFER_MB: u64 = 256; + +/// Conversion factor: 1 MB = 1024 * 1024 bytes (binary megabyte, used for RAM calculations) +pub const MB_TO_BYTES: u64 = 1024 * 1024; + +/// Conversion factor: 1 MB = 1000 * 1000 bytes (decimal megabyte, used by image crate) +/// The image crate's memory limits use decimal MB, not binary MB. +pub const DECIMAL_MB_TO_BYTES: u64 = 1000 * 1000; + +/// Maximum dimension for image decoding +pub const MAX_DIMENSION_FOR_DECODE: u32 = 65536; + +/// Get the dimensions of an image without fully decoding it +pub fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> { + match ImageReader::open(path) { + Ok(reader) => match reader.into_dimensions() { + Ok((width, height)) => { + log::debug!( + "Image dimensions: {}x{} for {}", + width, + height, + path.display() + ); + Some((width, height)) + } + Err(e) => { + log::warn!("Failed to get dimensions for {}: {}", path.display(), e); + None + } + }, + Err(e) => { + log::warn!("Failed to open image reader for {}: {}", path.display(), e); + None + } + } +} + +/// Check if there's sufficient memory to decode an image. +/// +/// This function performs two types of checks: +/// 1. System RAM availability (Linux only via procfs) +/// 2. GPU buffer limits (all platforms) +/// +/// Platform-specific behavior: +/// - Linux: Full RAM checking via /proc/meminfo + GPU checks +/// - Windows/macOS: GPU buffer checks only (RAM checking not yet implemented) +/// +/// Returns: (has_memory, error_message) +pub fn check_memory_available(width: u32, height: u32) -> (bool, Option) { + if width == 0 || height == 0 { + let error_msg = format!( + "Invalid image dimensions: {}x{} (zero dimension)", + width, height + ); + log::error!("{}", error_msg); + return (false, Some(error_msg)); + } + + let pixels = match (width as u64).checked_mul(height as u64) { + Some(p) => p, + None => { + let error_msg = format!( + "Image dimensions too large: {}x{} causes overflow in pixel calculation", + width, height + ); + log::error!("{}", error_msg); + return (false, Some(error_msg)); + } + }; + + let bytes_needed = match pixels.checked_mul(RGBA_BYTES_PER_PIXEL) { + Some(b) => b, + None => { + let error_msg = format!( + "Image memory requirements overflow: {}x{} pixels requires more than {} bytes", + width, + height, + u64::MAX + ); + log::error!("{}", error_msg); + return (false, Some(error_msg)); + } + }; + + // Add overhead for decode buffers, fragment allocations, and intermediate representations + let bytes_with_overhead = (bytes_needed as f64 * DECODE_OVERHEAD_FACTOR) as u64; + let mb_needed = bytes_with_overhead / MB_TO_BYTES; + + // Check system RAM availability (Linux only) + #[cfg(target_os = "linux")] + { + use procfs::Current; + match procfs::Meminfo::current() { + Ok(meminfo) => { + // MemAvailable includes reclaimable cache and is the best estimate of + // actually available memory for new allocations + let available_kb = meminfo.mem_available.unwrap_or(0); + let available_bytes = available_kb * 1024; + + // Maintain system reserve to prevent thrashing and OOM killer + let min_reserve_bytes = SYSTEM_MEMORY_RESERVE_MB * MB_TO_BYTES; + let usable_bytes = available_bytes.saturating_sub(min_reserve_bytes); + + if bytes_with_overhead > usable_bytes { + let available_mb = available_bytes / MB_TO_BYTES; + let error_msg = format!( + "Insufficient memory: need {}MB, available {}MB. Try closing other applications.", + mb_needed, available_mb + ); + log::warn!("{}", error_msg); + return (false, Some(error_msg)); + } + } + Err(e) => { + log::warn!("Failed to read /proc/meminfo: {}. Skipping RAM check.", e); + // Graceful fallback: continue to GPU checks + } + } + } + + // Note: RAM checking not implemented for Windows/macOS + // These platforms will only validate against GPU buffer limits below + #[cfg(not(target_os = "linux"))] + { + log::debug!( + "RAM checking not available on this platform. Only GPU limits will be enforced." + ); + } + + // Check GPU fragment/atlas tile limits + // Large images are split into atlas fragments for GPU upload. + // Each fragment must fit within GPU buffer size limits. + let fragment_bytes = + (ATLAS_FRAGMENT_SIZE as u64) * (ATLAS_FRAGMENT_SIZE as u64) * RGBA_BYTES_PER_PIXEL; + let max_gpu_buffer_bytes = MAX_GPU_BUFFER_MB * MB_TO_BYTES; + + let fragments_x = (width + ATLAS_FRAGMENT_SIZE - 1) / ATLAS_FRAGMENT_SIZE; + let fragments_y = (height + ATLAS_FRAGMENT_SIZE - 1) / ATLAS_FRAGMENT_SIZE; + let fragment_count = fragments_x as u64 * fragments_y as u64; + + // Fragments are uploaded sequentially, so we only need one fragment buffer at a time. + // However, each individual fragment must fit within GPU buffer size limits. + if fragment_bytes > max_gpu_buffer_bytes { + let max_dimension = (MAX_GPU_BUFFER_MB * MB_TO_BYTES / RGBA_BYTES_PER_PIXEL) as f64; + let max_dimension = (max_dimension.sqrt() as u32).saturating_sub(100); // Add safety margin + + let error_msg = format!( + "Image too large for GPU: {}x{} pixels exceeds GPU buffer limits. \ + Maximum supported dimension is approximately {}x{} pixels.", + width, height, max_dimension, max_dimension + ); + log::error!("{}", error_msg); + return (false, Some(error_msg)); + } + + log::debug!( + "Memory check passed: {}x{} image needs {}MB RAM, will use {} GPU fragment(s) of {}MB each", + width, + height, + mb_needed, + fragment_count, + fragment_bytes / MB_TO_BYTES + ); + + (true, None) +} + +/// Decode a large image asynchronously in a blocking thread pool. +/// +/// This function is used for gallery mode where full-resolution images need to be loaded. +/// It uses the full memory budget (GALLERY_MEMORY_LIMIT_MB) since only one image +/// decodes at a time in gallery mode. +pub async fn decode_large_image(path: PathBuf) -> Option<(PathBuf, u32, u32, Vec)> { + // Decode image in blocking thread pool (CPU-intensive work should not block) + tokio::task::spawn_blocking(move || { + log::info!("Starting async decode of {}", path.display()); + + // Use ImageReader with explicit memory limits to avoid "Memory limit exceeded" errors + // Gallery mode uses the full memory budget since only one image decodes at a time + match image::ImageReader::open(&path) { + Ok(reader) => { + match reader.with_guessed_format() { + Ok(mut reader) => { + // Note: image crate uses decimal MB (1000^2), not binary MB (1024^2) + let mut limits = image::Limits::default(); + limits.max_alloc = Some(GALLERY_MEMORY_LIMIT_MB * DECIMAL_MB_TO_BYTES); + reader.limits(limits); + + match reader.decode() { + Ok(img) => { + let rgba = img.into_rgba8(); + let width = rgba.width(); + let height = rgba.height(); + let pixels = rgba.into_raw(); + + log::info!( + "Decoded {}x{} image: {}", + width, + height, + path.display() + ); + Some((path, width, height, pixels)) + } + Err(e) => { + log::warn!("Failed to decode {}: {}", path.display(), e); + None + } + } + } + Err(e) => { + log::warn!("Failed to guess format for {}: {}", path.display(), e); + None + } + } + } + Err(e) => { + log::warn!("Failed to open {}: {}", path.display(), e); + None + } + } + }) + .await + .ok() + .flatten() +} + + +/// Manages state and operations for large image decoding in gallery mode +#[derive(Debug, Default)] +pub struct LargeImageManager { + /// Paths of images currently being decoded + decoding_images: HashSet, + /// Cache of decoded image handles + decoded_images: HashMap, + /// Errors encountered during decoding + decode_errors: HashMap, +} + +impl LargeImageManager { + pub fn new() -> Self { + Self::default() + } + + pub fn is_decoding(&self, path: &Path) -> bool { + self.decoding_images.contains(path) + } + + pub fn get_decoded(&self, path: &Path) -> Option<&widget::image::Handle> { + self.decoded_images.get(path) + } + + pub fn get_error(&self, path: &Path) -> Option<&String> { + self.decode_errors.get(path) + } + + pub fn mark_decoding(&mut self, path: PathBuf) { + self.decoding_images.insert(path); + } + + pub fn store_decoded(&mut self, path: PathBuf, handle: widget::image::Handle) { + self.decoded_images.insert(path.clone(), handle); + self.decoding_images.remove(&path); + } + + pub fn store_error(&mut self, path: PathBuf, error: String) { + self.decode_errors.insert(path, error); + } + + pub fn clear_error(&mut self, path: &Path) { + self.decode_errors.remove(path); + } + + pub fn clear_cache(&mut self) { + log::info!( + "Clearing {} cached images from large image manager", + self.decoded_images.len() + ); + self.decoded_images.clear(); + } + + pub fn cache_size(&self) -> usize { + self.decoded_images.len() + } + + pub fn cache_is_empty(&self) -> bool { + self.decoded_images.is_empty() + } +} diff --git a/src/lib.rs b/src/lib.rs index 6ffbe2e..e2bd675 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ use config::Config; pub mod config; pub mod dialog; mod key_bind; +pub(crate) mod large_image; mod localize; mod menu; mod mime_app; diff --git a/src/tab.rs b/src/tab.rs index 653c0ad..b97aeda 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -49,8 +49,6 @@ use icu::{ use image::{DynamicImage, ImageDecoder, ImageReader}; use jxl_oxide::integration::JxlDecoder; use mime_guess::{Mime, mime}; -#[cfg(target_os = "linux")] -use procfs::Current; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::{ @@ -81,6 +79,11 @@ use crate::{ config::{DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig, ThumbCfg}, dialog::DialogKind, fl, + large_image::{ + DECIMAL_MB_TO_BYTES, LargeImageManager, MAX_DIMENSION_FOR_DECODE, ATLAS_FRAGMENT_SIZE, + MB_TO_BYTES, RGBA_BYTES_PER_PIXEL, check_memory_available, decode_large_image, + get_image_dimensions, + }, localize::{LANGUAGE_SORTER, LOCALE}, menu, mime_app, mime_icon::{mime_for_path, mime_icon}, @@ -108,40 +111,6 @@ pub static THUMB_SEMAPHORE_NORMAL: LazyLock = pub static THUMB_SEMAPHORE_LARGE: LazyLock = LazyLock::new(|| tokio::sync::Semaphore::const_new(1)); -// Memory management constants -/// Bytes per pixel in RGBA format (Red, Green, Blue, Alpha = 4 bytes) -const RGBA_BYTES_PER_PIXEL: u64 = 4; - -/// Overhead factor for image decoding operations (30% additional memory for decode buffers, -/// fragment allocations, and intermediate representations during image decoding) -const DECODE_OVERHEAD_FACTOR: f64 = 1.3; - -/// System memory reserve in MB to maintain for system stability (prevents thrashing) -/// Note: RAM checking is currently only available on Linux via procfs. -/// On Windows and macOS, only GPU buffer limits are enforced. -const SYSTEM_MEMORY_RESERVE_MB: u64 = 500; - -/// Maximum memory allocation for gallery image decoding in MB. -/// Gallery mode uses the full memory budget since only one image decodes at a time. -/// This matches the ThumbCfg max_mem_mb budget for consistency. -const GALLERY_MEMORY_LIMIT_MB: u64 = 2000; - -/// Atlas fragment/tile size in pixels. Large images are split into fragments of this size. -/// Must match the atlas SIZE constant in libcosmic/iced/wgpu/src/image/atlas.rs -const ATLAS_FRAGMENT_SIZE: u32 = 4096; - -/// Conservative GPU buffer size limit in MB. Each atlas fragment can be up to this size. -/// Based on wgpu device limits - most GPUs support at least 256MB buffers. -/// Reference: https://docs.rs/wgpu/latest/wgpu/struct.Limits.html#structfield.max_buffer_size -const MAX_GPU_BUFFER_MB: u64 = 256; - -/// Conversion factor: 1 MB = 1024 * 1024 bytes (binary megabyte, used for RAM calculations) -const MB_TO_BYTES: u64 = 1024 * 1024; - -/// Conversion factor: 1 MB = 1000 * 1000 bytes (decimal megabyte, used by image crate) -/// The image crate's memory limits use decimal MB, not binary MB. -const DECIMAL_MB_TO_BYTES: u64 = 1000 * 1000; - pub(crate) static SORT_OPTION_FALLBACK: LazyLock> = LazyLock::new(|| { FxHashMap::from_iter(dirs::download_dir().into_iter().map(|dir| { @@ -1803,7 +1772,7 @@ impl ItemThumbnail { // Create and cache the full-size handle for large images that need GPU tiling // Images >4096 pixels get fragmented into multiple tiles for GPU upload let full_handle = original_dims - .filter(|(w, h)| *w > 4096 || *h > 4096) + .filter(|(w, h)| *w > ATLAS_FRAGMENT_SIZE || *h > ATLAS_FRAGMENT_SIZE) .map(|_| widget::image::Handle::from_path(path)); return Self::Image( @@ -1852,7 +1821,6 @@ impl ItemThumbnail { // Check for extremely large dimensions that would cause memory issues during decoding // The GPU tiling system can handle large images, but we still need to decode them first // Set a reasonable limit to prevent OOM during image decoding - const MAX_DIMENSION_FOR_DECODE: u32 = 65536; // 64K pixels is generous let dimensions_ok = match image::image_dimensions(path) { Ok((width, height)) => { if width > MAX_DIMENSION_FOR_DECODE || height > MAX_DIMENSION_FOR_DECODE { @@ -1865,7 +1833,7 @@ impl ItemThumbnail { ); false } else { - if width > 8192 || height > 8192 { + if width > ATLAS_FRAGMENT_SIZE || height > ATLAS_FRAGMENT_SIZE { log::info!( "Large image {}x{} detected, will use GPU tiling for display", width, @@ -1944,11 +1912,12 @@ impl ItemThumbnail { if let Some(dyn_img) = dyn_img { let (img_width, img_height) = (dyn_img.width(), dyn_img.height()); - let full_handle = if img_width > 4096 || img_height > 4096 { - Some(widget::image::Handle::from_path(path)) - } else { - None - }; + let full_handle = + if img_width > ATLAS_FRAGMENT_SIZE || img_height > ATLAS_FRAGMENT_SIZE { + Some(widget::image::Handle::from_path(path)) + } else { + None + }; if let Ok(cacher) = thumbnail_cacher.as_ref() { match cacher.update_with_image(dyn_img) { @@ -2593,221 +2562,7 @@ pub struct Tab { time_formatter: DateTimeFormatter, watch_drag: bool, window_id: Option, - decoding_images: std::collections::HashSet, - decoded_images: std::collections::HashMap, - decode_errors: std::collections::HashMap, -} - -fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> { - match ImageReader::open(path) { - Ok(reader) => match reader.into_dimensions() { - Ok((width, height)) => { - log::debug!( - "Image dimensions: {}x{} for {}", - width, - height, - path.display() - ); - Some((width, height)) - } - Err(e) => { - log::warn!("Failed to get dimensions for {}: {}", path.display(), e); - None - } - }, - Err(e) => { - log::warn!("Failed to open image reader for {}: {}", path.display(), e); - None - } - } -} - -/// Check if there's sufficient memory to decode an image. -/// -/// This function performs two types of checks: -/// 1. System RAM availability (Linux only via procfs) -/// 2. GPU buffer limits (all platforms) -/// -/// Platform-specific behavior: -/// - Linux: Full RAM checking via /proc/meminfo + GPU checks -/// - Windows/macOS: GPU buffer checks only (RAM checking not yet implemented) -/// -fn check_memory_available(width: u32, height: u32) -> (bool, Option) { - if width == 0 || height == 0 { - let error_msg = format!( - "Invalid image dimensions: {}x{} (zero dimension)", - width, height - ); - log::error!("{}", error_msg); - return (false, Some(error_msg)); - } - - let pixels = match (width as u64).checked_mul(height as u64) { - Some(p) => p, - None => { - let error_msg = format!( - "Image dimensions too large: {}x{} causes overflow in pixel calculation", - width, height - ); - log::error!("{}", error_msg); - return (false, Some(error_msg)); - } - }; - - let bytes_needed = match pixels.checked_mul(RGBA_BYTES_PER_PIXEL) { - Some(b) => b, - None => { - let error_msg = format!( - "Image memory requirements overflow: {}x{} pixels requires more than {} bytes", - width, - height, - u64::MAX - ); - log::error!("{}", error_msg); - return (false, Some(error_msg)); - } - }; - - // Add overhead for decode buffers, fragment allocations, and intermediate representations - let bytes_with_overhead = (bytes_needed as f64 * DECODE_OVERHEAD_FACTOR) as u64; - let mb_needed = bytes_with_overhead / MB_TO_BYTES; - - // Check system RAM availability (Linux only) - #[cfg(target_os = "linux")] - { - match procfs::Meminfo::current() { - Ok(meminfo) => { - // MemAvailable includes reclaimable cache and is the best estimate of - // actually available memory for new allocations - let available_kb = meminfo.mem_available.unwrap_or(0); - let available_bytes = available_kb * 1024; - - // Maintain system reserve to prevent thrashing and OOM killer - let min_reserve_bytes = SYSTEM_MEMORY_RESERVE_MB * MB_TO_BYTES; - let usable_bytes = available_bytes.saturating_sub(min_reserve_bytes); - - if bytes_with_overhead > usable_bytes { - let available_mb = available_bytes / MB_TO_BYTES; - let error_msg = format!( - "Insufficient memory: need {}MB, available {}MB. Try closing other applications.", - mb_needed, available_mb - ); - log::warn!("{}", error_msg); - return (false, Some(error_msg)); - } - } - Err(e) => { - log::warn!("Failed to read /proc/meminfo: {}. Skipping RAM check.", e); - // Graceful fallback: continue to GPU checks - } - } - } - - // Note: RAM checking not implemented for Windows/macOS - // These platforms will only validate against GPU buffer limits below - #[cfg(not(target_os = "linux"))] - { - log::debug!( - "RAM checking not available on this platform. Only GPU limits will be enforced." - ); - } - - // Check GPU fragment/atlas tile limits - // Large images are split into atlas fragments for GPU upload. - // Each fragment must fit within GPU buffer size limits. - let fragment_bytes = - (ATLAS_FRAGMENT_SIZE as u64) * (ATLAS_FRAGMENT_SIZE as u64) * RGBA_BYTES_PER_PIXEL; - let max_gpu_buffer_bytes = MAX_GPU_BUFFER_MB * MB_TO_BYTES; - - let fragments_x = (width + ATLAS_FRAGMENT_SIZE - 1) / ATLAS_FRAGMENT_SIZE; - let fragments_y = (height + ATLAS_FRAGMENT_SIZE - 1) / ATLAS_FRAGMENT_SIZE; - let fragment_count = fragments_x as u64 * fragments_y as u64; - - // Fragments are uploaded sequentially, so we only need one fragment buffer at a time. - // However, each individual fragment must fit within GPU buffer size limits. - if fragment_bytes > max_gpu_buffer_bytes { - let max_dimension = (MAX_GPU_BUFFER_MB * MB_TO_BYTES / RGBA_BYTES_PER_PIXEL) as f64; - let max_dimension = (max_dimension.sqrt() as u32).saturating_sub(100); // Add safety margin - - let error_msg = format!( - "Image too large for GPU: {}x{} pixels exceeds GPU buffer limits. \ - Maximum supported dimension is approximately {}x{} pixels.", - width, height, max_dimension, max_dimension - ); - log::error!("{}", error_msg); - return (false, Some(error_msg)); - } - - log::debug!( - "Memory check passed: {}x{} image needs {}MB RAM, will use {} GPU fragment(s) of {}MB each", - width, - height, - mb_needed, - fragment_count, - fragment_bytes / MB_TO_BYTES - ); - - (true, None) -} - -/// Decode a large image asynchronously in a blocking thread pool. -/// -/// This function is used for gallery mode where full-resolution images need to be loaded. -/// It uses the full memory budget (GALLERY_MEMORY_LIMIT_MB) since only one image -/// decodes at a time in gallery mode. -/// -async fn decode_large_image(path: PathBuf) -> Option<(PathBuf, u32, u32, Vec)> { - // Decode image in blocking thread pool (CPU-intensive work should not block async runtime) - tokio::task::spawn_blocking(move || { - log::info!("Starting async decode of {}", path.display()); - - // Use ImageReader with explicit memory limits to avoid "Memory limit exceeded" errors - // Gallery mode uses the full memory budget since only one image decodes at a time - match image::ImageReader::open(&path) { - Ok(reader) => { - match reader.with_guessed_format() { - Ok(mut reader) => { - // Note: image crate uses decimal MB (1000^2), not binary MB (1024^2) - let mut limits = image::Limits::default(); - limits.max_alloc = Some(GALLERY_MEMORY_LIMIT_MB * DECIMAL_MB_TO_BYTES); - reader.limits(limits); - - match reader.decode() { - Ok(img) => { - let rgba = img.into_rgba8(); - let width = rgba.width(); - let height = rgba.height(); - let pixels = rgba.into_raw(); - - log::info!( - "Decoded {}x{} image: {}", - width, - height, - path.display() - ); - Some((path, width, height, pixels)) - } - Err(e) => { - log::warn!("Failed to decode {}: {}", path.display(), e); - None - } - } - } - Err(e) => { - log::warn!("Failed to guess format for {}: {}", path.display(), e); - None - } - } - } - Err(e) => { - log::warn!("Failed to open {}: {}", path.display(), e); - None - } - } - }) - .await - .ok() - .flatten() + large_image_manager: LargeImageManager, } async fn calculate_dir_size(path: &Path, controller: Controller) -> Result { @@ -2929,9 +2684,7 @@ impl Tab { time_formatter: time_formatter(config.military_time), watch_drag: true, window_id, - decoding_images: std::collections::HashSet::new(), - decoded_images: std::collections::HashMap::new(), - decode_errors: std::collections::HashMap::new(), + large_image_manager: LargeImageManager::new(), } } @@ -3239,11 +2992,11 @@ impl Tab { if let Some(ItemThumbnail::Image(_, _, _full_handle_opt)) = &item.thumbnail_opt { if let Some(path) = item.path_opt() { - self.decode_errors.remove(path); + self.large_image_manager.clear_error(path); // Only decode if not already decoded or decoding - if !self.decoded_images.contains_key(path) - && !self.decoding_images.contains(path) + if self.large_image_manager.get_decoded(path).is_none() + && !self.large_image_manager.is_decoding(path) { if let Some((width, height)) = get_image_dimensions(path) { let (has_memory, error_opt) = @@ -3251,20 +3004,20 @@ impl Tab { if !has_memory { // Insufficient memory --> try clearing cache - if !self.decoded_images.is_empty() { + if !self.large_image_manager.cache_is_empty() { log::info!( "Insufficient memory, clearing {} cached images", - self.decoded_images.len() + self.large_image_manager.cache_size() ); - self.decoded_images.clear(); + self.large_image_manager.clear_cache(); // Check again after clearing cache let (has_memory_after_clear, error_opt_after) = check_memory_available(width, height); if !has_memory_after_clear { if let Some(error_msg) = error_opt_after { - self.decode_errors - .insert(path.clone(), error_msg); + self.large_image_manager + .store_error(path.clone(), error_msg); log::warn!( "Cannot load {}: insufficient memory even after cache clear", path.display() @@ -3277,7 +3030,8 @@ impl Tab { ); } else { if let Some(error_msg) = error_opt { - self.decode_errors.insert(path.clone(), error_msg); + self.large_image_manager + .store_error(path.clone(), error_msg); log::warn!( "Cannot load {}: insufficient memory and cache is empty", path.display() @@ -3287,7 +3041,7 @@ impl Tab { } } - self.decoding_images.insert(path.clone()); + self.large_image_manager.mark_decoding(path.clone()); let path_clone = path.clone(); commands.push(Command::Iced( @@ -3307,7 +3061,7 @@ impl Tab { .into(), )); } else { - self.decode_errors.insert( + self.large_image_manager.store_error( path.clone(), "Failed to read image dimensions".to_string(), ); @@ -4274,11 +4028,8 @@ impl Tab { // Create handle from pre-decoded RGBA data (fast!) let handle = widget::image::Handle::from_rgba(width, height, pixels); - // Store decoded image handle - self.decoded_images.insert(path.clone(), handle); - - // Remove from decoding set - self.decoding_images.remove(&path); + // Store decoded image handle and remove from decoding set + self.large_image_manager.store_decoded(path, handle); } Message::ToggleSort(heading_option) => { if !matches!(self.location, Location::Search(..)) { @@ -4639,12 +4390,14 @@ impl Tab { let (image_handle, is_loading, error_msg_opt) = if let Some(path) = item.path_opt() { - if let Some(error_msg) = self.decode_errors.get(path) { + if let Some(error_msg) = self.large_image_manager.get_error(path) { (handle, false, Some(error_msg.clone())) - } else if let Some(decoded_handle) = self.decoded_images.get(path) { + } else if let Some(decoded_handle) = + self.large_image_manager.get_decoded(path) + { // Full resolution ready --> use it (decoded_handle, false, None) - } else if self.decoding_images.contains(path) { + } else if self.large_image_manager.is_decoding(path) { // Currently decoding --> show thumbnail with loading indicator (handle, true, None) } else if let Some(full_handle) = full_handle_opt {