use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::mem;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::time::SystemTime;
use log::trace;
use gelatin::{
glium::{
self,
texture::{MipmapsOption, RawImage2d, SrgbTexture2d},
CapabilitiesSource,
},
image,
};
pub mod image_loader;
use self::{directory::DirItem, image_loader::*};
mod pending_requests;
use pending_requests::PendingRequests;
mod directory;
use directory::Directory;
pub mod errors {
use crate::image_cache::image_loader;
use gelatin::glium::texture;
use gelatin::image;
use std::io;
error_chain! {
foreign_links {
Io(io::Error) #[doc = "Error during IO"];
TextureCreationError(texture::TextureCreationError);
ImageRsError(image::ImageError);
TextureLoaderError(image_loader::errors::Error);
DirError(super::directory::Error);
}
errors {
WaitingOnLoader {
description("ImageCache is waiting for loader to send result")
display("ImageCache is waiting for loader to send result")
}
WaitingOnDirFilter {
display("ImageCache is waiting for the directory items to be filtered for image files")
}
FailedToLoadImage(req_id: u32) {
display("Failed to load #{}", req_id)
}
}
}
}
pub use self::errors::Result;
use self::errors::*;
pub fn get_image_size_estimate(width: u32, height: u32) -> isize {
(width * height * 6) as isize
}
pub fn get_anim_size_estimate(frames: &[AnimationFrameTexture]) -> isize {
frames.iter().map(|frame| get_image_size_estimate(frame.w, frame.h)).sum()
}
enum RequestKind<'a> {
NonPriority,
Priority { display: &'a glium::Display },
}
impl<'a> RequestKind<'a> {
pub fn priority(self) -> bool {
match self {
RequestKind::Priority { .. } => true,
RequestKind::NonPriority => false,
}
}
}
pub struct TextureGridItem {
pub tex: SrgbTexture2d,
pub col: u32,
pub row: u32,
}
#[derive(Clone)]
pub struct AnimationFrameTexture {
pub tex_grid: Rc<Vec<TextureGridItem>>,
pub cell_step_size: u32,
pub grid_rows: u32,
pub grid_cols: u32,
pub delay_nano: u64,
pub orientation: Orientation,
pub w: u32,
pub h: u32,
}
impl AnimationFrameTexture {
pub fn from_image(
display: &glium::Display,
image: image::RgbaImage,
delay_nano: u64,
orientation: Orientation,
) -> Result<Self> {
let (w, h) = image.dimensions();
let img_bytes = image.into_raw();
let mut tex_grid = Vec::new();
let max_size = (display.get_capabilities().max_texture_size as u32 / 2).min(4 * 1024);
let grid_cols = ((w - 1) / max_size) + 1;
let grid_rows = ((h - 1) / max_size) + 1;
for row in 0..grid_rows {
for col in 0..grid_cols {
let offset_x = col * max_size;
let offset_y = row * max_size;
let cell_w = (w - offset_x).min(max_size);
let cell_h = (h - offset_y).min(max_size);
let tex = texture_from_img_rect(
display, w, h, &img_bytes, offset_x, offset_y, cell_w, cell_h,
)?;
let item = TextureGridItem { tex, col, row };
tex_grid.push(item);
}
}
Ok(AnimationFrameTexture {
tex_grid: Rc::new(tex_grid),
delay_nano,
orientation,
w,
h,
cell_step_size: max_size,
grid_rows,
grid_cols,
})
}
pub fn oriented_dimensions(&self) -> (u32, u32) {
use Orientation::*;
match self.orientation {
Deg0 | Deg0HorFlip | Deg180 | Deg180HorFlip => (self.w, self.h),
Deg90 | Deg90VerFlip | Deg270 | Deg270VerFlip => (self.h, self.w),
}
}
}
#[allow(clippy::too_many_arguments)]
fn texture_from_img_rect(
display: &glium::Display,
img_w: u32,
img_h: u32,
img_bytes: &[u8],
offset_x: u32,
offset_y: u32,
cell_w: u32,
cell_h: u32,
) -> Result<SrgbTexture2d> {
let raw_image;
if img_w == cell_w {
assert!(offset_x == 0);
let start = (offset_y as usize * img_w as usize) * 4;
let end = start + (cell_h as usize * cell_w as usize * 4);
raw_image = RawImage2d {
data: Cow::Borrowed(&img_bytes[start..end]),
format: glium::texture::ClientFormat::U8U8U8U8,
width: cell_w,
height: cell_h,
};
} else {
let cell_size = cell_w as usize * cell_h as usize * 4;
let mut cell_pixels = Vec::with_capacity(cell_size);
for y in offset_y..(offset_y + cell_h) {
let start = (y as usize * img_w as usize + offset_x as usize) * 4;
let end = start + (cell_w as usize * 4);
cell_pixels.extend_from_slice(&img_bytes[start..end]);
}
raw_image = RawImage2d::from_raw_rgba(cell_pixels, (cell_w, cell_h));
}
let x_pow = 31 - img_w.leading_zeros();
let y_pow = 31 - img_h.leading_zeros();
let max_mipmap_levels = x_pow.min(y_pow).min(4);
let mipmaps = if max_mipmap_levels == 1 {
MipmapsOption::NoMipmap
} else {
MipmapsOption::AutoGeneratedMipmapsMax(max_mipmap_levels)
};
Ok(SrgbTexture2d::with_mipmaps(display, raw_image, mipmaps)?)
}
struct CachedTexture {
_req_id: u32,
needs_update: bool,
mod_time: Option<SystemTime>,
fully_loaded: bool,
failed: bool,
frames: Vec<AnimationFrameTexture>,
}
pub struct ImageCache {
dir: Directory,
current_frame_idx: usize,
remaining_capacity: isize,
total_capacity: isize,
curr_est_size: isize,
pending_requests: PendingRequests,
texture_cache: BTreeMap<u32, CachedTexture>,
loader: ImageLoader,
}
impl ImageCache {
const MAX_PENDING_REQUESTS: usize = 5;
pub fn new(capacity: isize, threads: u32) -> ImageCache {
ImageCache {
dir: Directory::new(),
current_frame_idx: 0,
remaining_capacity: capacity,
total_capacity: capacity,
curr_est_size: 1000,
pending_requests: PendingRequests::new(),
texture_cache: BTreeMap::new(),
loader: ImageLoader::new(threads),
}
}
pub fn current_filename(&self) -> Option<OsString> {
self.dir.curr_filename()
}
pub fn current_file_path(&self) -> Option<PathBuf> {
if let Some(filename) = self.current_filename() {
Some(self.dir.path().join(filename))
} else {
None
}
}
pub fn current_file_index(&mut self) -> Option<usize> {
self.dir.curr_img_index()
}
pub fn current_dir_len(&mut self) -> Option<usize> {
self.dir.image_count()
}
fn curr_dir_item(&self) -> Result<DirItem> {
if let Some(desc) = self.dir.curr_descriptor() {
Ok(desc.clone())
} else {
bail!("Could not get the current file descriptor");
}
}
pub fn loaded_still_image(&self) -> bool {
if let Some(desc) = self.dir.curr_descriptor() {
if let Some(img) = self.texture_cache.get(&desc.request_id) {
if img.fully_loaded && img.frames.len() == 1 {
return true;
}
}
}
false
}
pub fn update_directory(&mut self) -> Result<()> {
self.dir.update_directory()?;
for texture in self.texture_cache.values_mut() {
texture.needs_update = true;
}
Ok(())
}
pub fn load_at_index(
&mut self,
display: &glium::Display,
index: usize,
frame_id: Option<isize>,
) -> Result<(AnimationFrameTexture, PathBuf)> {
let path = self
.dir
.image_by_index(index)
.ok_or_else(|| Error::from_kind(ErrorKind::WaitingOnDirFilter))?
.path
.clone();
let result = self.load_specific(display, &path, frame_id)?;
Ok((result, path))
}
pub fn load_specific(
&mut self,
display: &glium::Display,
path: &Path,
frame_id: Option<isize>,
) -> Result<AnimationFrameTexture> {
trace!("Begin `load_specific`");
self.receive_prefetched();
trace!("Receive prefetched done");
let target_file_name;
let parent;
if path.is_dir() {
parent = path.to_owned();
target_file_name = None;
} else {
let filename_and_parent = get_file_name_and_parent(path)?;
target_file_name = Some(filename_and_parent.0);
parent = filename_and_parent.1;
}
let prev_img_index = self.dir.curr_img_index();
if let Some(target_file_name) = target_file_name {
self.change_directory_with_filename(&parent, &target_file_name)?;
} else {
self.change_directory(&parent)?;
self.current_frame_idx = 0;
}
if self.dir.path() != parent {
let DirItem { path, request_id } = self.curr_dir_item()?;
self.send_request_for_file(path, request_id, RequestKind::Priority { display });
return Err(errors::Error::from_kind(errors::ErrorKind::WaitingOnLoader));
}
if let Some(img_index) = self.dir.curr_img_index() {
self.dir.set_curr_img_index(img_index)?;
}
let requested_frame_id = match frame_id {
Some(frame_id) => frame_id,
None => {
let mut retval = 0;
if let Some(prev_img_index) = prev_img_index {
if let Some(curr_img_index) = self.dir.curr_img_index() {
if curr_img_index == prev_img_index {
retval = self.current_frame_idx as isize;
}
}
}
retval
}
};
self.refresh_cache();
self.try_getting_requested_image(display, requested_frame_id)
}
fn refresh_cache(&mut self) {
trace!("Begin `refresh_cache`");
if let Some(curr_index) = self.dir.curr_img_index() {
let cache = mem::take(&mut self.texture_cache);
let mut sorted_files: Vec<_> = cache.into_iter().enumerate().collect();
sorted_files
.sort_unstable_by_key(|&(index, _)| (index as isize - curr_index as isize).abs());
self.remaining_capacity = self.total_capacity;
sorted_files.retain(|(_, (_, texture))| {
let all_frames_size = get_anim_size_estimate(&texture.frames);
if self.remaining_capacity > (all_frames_size + self.curr_est_size) {
self.remaining_capacity -= all_frames_size;
true
} else {
false
}
});
self.texture_cache = sorted_files.into_iter().map(|(_, entry)| entry).collect();
}
}
pub fn load_next(
&mut self,
display: &glium::Display,
) -> Result<(AnimationFrameTexture, PathBuf)> {
self.load_jump(display, 1, 0)
}
pub fn load_prev(
&mut self,
display: &glium::Display,
) -> Result<(AnimationFrameTexture, PathBuf)> {
self.load_jump(display, -1, 0)
}
pub fn load_jump(
&mut self,
display: &glium::Display,
file_jump_count: i32,
frame_jump_count: isize,
) -> Result<(AnimationFrameTexture, PathBuf)> {
if file_jump_count == 0 {
let target_frame = self.current_frame_idx as isize + frame_jump_count;
let requested = self.try_getting_requested_image(display, target_frame);
if let Some(path) = self.current_file_path() {
return requested.map(|t| (t, path));
} else {
bail!("No file is open");
}
} else {
self.current_frame_idx = 0;
}
let target_path;
if file_jump_count.abs() == 1 {
if file_jump_count > 0 {
self.dir.jump_to_next();
} else {
self.dir.jump_to_prev();
}
target_path = self.dir.curr_descriptor().unwrap().path.clone();
} else if let (Some(curr_index), Some(img_count)) =
(self.dir.curr_img_index(), self.dir.image_count())
{
let target_index = (curr_index as isize + file_jump_count as isize)
.rem_euclid(img_count as isize) as usize;
target_path = self.dir.image_by_index(target_index).unwrap().path.clone();
} else {
bail!("Folder is empty, no folder was open, or folder hasn't finished filtering when trying to jump to an image by index.");
}
let result = self.load_specific(display, &target_path, None)?;
Ok((result, target_path))
}
fn receive_prefetched(&mut self) {
use std::sync::mpsc::TryRecvError;
loop {
match self.loader.try_recv_prefetched() {
Ok(load_result) => {
self.pending_requests.add_load_result(load_result);
}
Err(TryRecvError::Disconnected) => panic!("Channel disconnected unexpectidly."),
Err(TryRecvError::Empty) => break,
}
}
}
pub fn process_prefetched(&mut self, display: &glium::Display) -> Result<()> {
self.receive_prefetched();
let mut uploaded_one = false;
let req_ids = self.pending_requests.get_all_ids();
let mut retval = Ok(());
for id in req_ids {
if let Some(results) = self.pending_requests.take_results(id) {
for result in results {
match self.upload_to_texture(display, result) {
Ok(_) => uploaded_one = true,
Err(Error(ErrorKind::FailedToLoadImage(..), ..)) => {}
Err(e) => {
retval = Err(e);
break;
}
}
}
}
if retval.is_err() {
return retval;
}
if uploaded_one {
break;
}
}
Ok(())
}
fn try_getting_requested_image(
&mut self,
display: &glium::Display,
frame_id: isize,
) -> Result<AnimationFrameTexture> {
trace!("Begin `try_getting_requested_image` in `image_cache`");
let DirItem { path, request_id: req_id } = self.curr_dir_item()?;
if let Some(results) = self.pending_requests.take_results(req_id) {
for load_result in results {
self.upload_to_texture(display, load_result)?;
}
}
if let Some(tex) = self.texture_cache.get(&req_id) {
if tex.failed {
return Err(Error::from_kind(ErrorKind::FailedToLoadImage(req_id)));
}
let modified = fs::metadata(&path).ok().and_then(|m| m.modified().ok());
let mut get_from_cache = false;
if let Some(curr_mod_time) = modified {
if let Some(mod_time) = tex.mod_time {
if mod_time == curr_mod_time {
get_from_cache = true;
}
}
} else {
get_from_cache = true;
}
if get_from_cache {
let count = tex.frames.len() as isize;
if tex.fully_loaded || (frame_id >= 0 && frame_id < count) {
let wrapped_id;
if frame_id < 0 {
wrapped_id = count + (frame_id % count);
} else {
wrapped_id = frame_id % count;
}
if let Some(frame) = tex.frames.get(wrapped_id as usize) {
self.current_frame_idx = wrapped_id as usize;
return Ok(frame.clone());
}
}
}
return Err(Error::from_kind(ErrorKind::WaitingOnLoader));
}
if self.pending_requests.contains(&req_id) {
PRIORITY_REQUEST_ID.store(req_id, Ordering::SeqCst);
return Err(Error::from_kind(ErrorKind::WaitingOnLoader));
}
self.send_request_for_file(path, req_id, RequestKind::Priority { display });
Err(Error::from_kind(ErrorKind::WaitingOnLoader))
}
fn upload_to_texture(
&mut self,
display: &glium::Display,
load_result: LoadResult,
) -> Result<Option<AnimationFrameTexture>> {
use std::collections::btree_map::Entry;
match load_result {
LoadResult::Start { req_id, metadata } => {
let curr_mod_time = metadata.modified().ok();
if let Some(cancelled) = self.pending_requests.cancelled(&req_id) {
if cancelled {
return Ok(None);
}
} else {
return Ok(None);
}
match self.texture_cache.entry(req_id) {
Entry::Vacant(entry) => {
entry.insert(CachedTexture {
_req_id: req_id,
needs_update: false,
fully_loaded: false,
mod_time: curr_mod_time,
failed: false,
frames: Vec::new(),
});
}
Entry::Occupied(mut entry) => {
let mut overwrite = true;
if let Some(curr_mod_time) = curr_mod_time {
let cached = entry.get();
if let Some(existing_mod_time) = cached.mod_time {
if existing_mod_time == curr_mod_time {
overwrite = false;
}
}
}
if overwrite {
let old_size_estimate = get_anim_size_estimate(&entry.get().frames);
self.remaining_capacity += old_size_estimate;
let mut_entry = entry.get_mut();
mut_entry.frames.clear();
mut_entry.mod_time = curr_mod_time;
}
}
}
Ok(None)
}
LoadResult::Frame { req_id, image, delay_nano, orientation } => {
if let Some(cancelled) = self.pending_requests.cancelled(&req_id) {
if cancelled {
return Ok(None);
}
} else {
return Ok(None);
}
let size_estimate = get_image_size_estimate(image.width(), image.height());
if let Some(entry) = self.texture_cache.get_mut(&req_id) {
let anim_frame =
AnimationFrameTexture::from_image(display, image, delay_nano, orientation)?;
entry.frames.push(anim_frame.clone());
self.remaining_capacity -= size_estimate;
return Ok(Some(anim_frame));
}
Ok(None)
}
LoadResult::Done { req_id } => {
if let Some(tex) = self.texture_cache.get_mut(&req_id) {
tex.fully_loaded = true;
}
let _ = PRIORITY_REQUEST_ID.compare_exchange(
req_id,
NON_EXISTENT_REQUEST_ID,
Ordering::SeqCst,
Ordering::SeqCst,
);
self.pending_requests.set_finished(&req_id);
Ok(None)
}
LoadResult::Failed { req_id } => {
if let Some(tex) = self.texture_cache.get_mut(&req_id) {
tex.fully_loaded = true;
tex.failed = true;
}
let _ = PRIORITY_REQUEST_ID.compare_exchange(
req_id,
NON_EXISTENT_REQUEST_ID,
Ordering::SeqCst,
Ordering::SeqCst,
);
self.pending_requests.set_finished(&req_id);
Err(errors::Error::from_kind(errors::ErrorKind::FailedToLoadImage(req_id)))
}
}
}
pub fn prefetch_neighbors(&mut self) {
if let Some(mut index) = self.dir.curr_img_index() {
let mut estimated_remaining_cap = self.remaining_capacity;
while estimated_remaining_cap > self.curr_est_size {
index += 1;
if self.prefetch_at_index(index) {
estimated_remaining_cap -= self.curr_est_size;
} else {
break;
}
}
}
}
pub fn prefetch_at_index(&mut self, index: usize) -> bool {
if self.remaining_capacity > self.curr_est_size {
let params = if let Some(desc) = self.dir.image_by_index(index) {
Some((desc.path.clone(), desc.request_id))
} else {
None
};
if let Some((path, req_id)) = params {
return self.send_request_for_file(path, req_id, RequestKind::NonPriority);
} else {
return false;
}
}
false
}
fn send_request_for_file(
&mut self,
file_path: PathBuf,
req_id: u32,
kind: RequestKind,
) -> bool {
if let RequestKind::Priority { display } = kind {
if self.pending_requests.len() >= Self::MAX_PENDING_REQUESTS {
if let Err(e) = self.process_prefetched(display) {
eprintln!("Error while processing prefetched images:\n{}", e);
}
}
}
if self.pending_requests.len() >= Self::MAX_PENDING_REQUESTS {
return false;
}
let mut cache_enty_invalid = false;
if let Some(texture) = self.texture_cache.get_mut(&req_id) {
if !texture.needs_update {
return false;
} else {
texture.needs_update = false;
if let Some(existing_mod_time) = texture.mod_time {
let new_mod_time =
fs::metadata(&file_path).ok().and_then(|m| m.modified().ok());
if let Some(new_mod_time) = new_mod_time {
if new_mod_time == existing_mod_time {
return false;
} else {
cache_enty_invalid = true;
}
}
}
}
}
if cache_enty_invalid {
self.texture_cache.remove(&req_id);
}
if kind.priority() {
PRIORITY_REQUEST_ID.store(req_id, Ordering::SeqCst);
}
if self.pending_requests.contains(&req_id) {
return false;
}
let request = LoadRequest { req_id, path: file_path };
self.pending_requests.add_request(request.clone());
self.loader.send_load_request(request);
true
}
fn change_directory(&mut self, dir_path: &Path) -> Result<()> {
if self.dir.path() == dir_path {
return Ok(());
}
self.texture_cache.clear();
self.remaining_capacity = self.total_capacity;
for (_, request) in self.pending_requests.iter_mut() {
request.cancel();
}
self.dir.change_directory(dir_path)?;
Ok(())
}
fn change_directory_with_filename(&mut self, dir_path: &Path, filename: &OsStr) -> Result<()> {
self.dir
.change_directory_with_filename(dir_path, filename)
.map_err(|e| Error::from_kind(ErrorKind::Msg(format!("{}", e))))
}
}
fn get_file_name_and_parent(path: &Path) -> Result<(OsString, PathBuf)> {
let file_name = match path.file_name() {
Some(f) => f.to_owned(),
None => bail!("Could not get file name from path {:?}", path),
};
let parent = match path.parent() {
Some(p) => {
if p == Path::new("") {
Path::new(".").canonicalize()?
} else {
p.canonicalize()?
}
}
None => {
let mut path = path.canonicalize()?;
if !path.pop() {
bail!("Could not get parent directory of {:?}", path);
}
path
}
};
Ok((file_name, parent))
}