use std::{
borrow::Cow,
collections::BTreeMap,
ffi::{OsStr, OsString},
fs, mem,
path::{Path, PathBuf},
rc::Rc,
sync::atomic::Ordering,
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;
pub mod directory;
use directory::Directory;
#[derive(Debug, thiserror::Error)]
pub enum TextureError {
#[error("ImageCache is waiting for the image loader to send result")]
WaitingOnLoader,
#[error("Other texture error: {0}")]
Other(Cow<'static, str>),
}
impl From<directory::Error> for TextureError {
fn from(value: directory::Error) -> Self {
Self::Other(format!("Directory error: {}", value).into())
}
}
impl From<glium::texture::TextureCreationError> for TextureError {
fn from(value: glium::texture::TextureCreationError) -> Self {
Self::Other(format!("glium failed to create the texture: {value}").into())
}
}
impl From<std::io::Error> for TextureError {
fn from(value: std::io::Error) -> Self {
Self::Other(format!("IO error occured during texture creation: {value}").into())
}
}
impl TextureError {
pub fn from_failed_request(req_id: u32) -> Self {
Self::Other(format!("Failed to load #{req_id}").into())
}
}
pub type TextureResult<T> = std::result::Result<T, TextureError>;
#[derive(Debug, thiserror::Error)]
pub enum PathResolutionError {
#[error("No image path has been specified. (Eg emulsion was started without it being requested to open an image or folder)")]
NotYetSpecified,
#[error("Could not find file path matching the request, because the Directory Filter hasn't finished yet")]
WaitingOnDirFilter,
}
pub type PathedTextureResult =
std::result::Result<(PathBuf, TextureResult<AnimationFrameTexture>), PathResolutionError>;
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 gelatin::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 delay_nano: u64,
pub orientation: Orientation,
pub w: u32,
pub h: u32,
}
impl AnimationFrameTexture {
pub fn from_image(
display: &gelatin::Display,
image: image::RgbaImage,
delay_nano: u64,
orientation: Orientation,
) -> TextureResult<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,
})
}
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: &gelatin::Display,
img_w: u32,
img_h: u32,
img_bytes: &[u8],
offset_x: u32,
offset_y: u32,
cell_w: u32,
cell_h: u32,
) -> TextureResult<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> {
self.current_filename().map(|name| self.dir.path().join(name))
}
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) -> Option<DirItem> {
self.dir.curr_descriptor().cloned()
}
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) -> directory::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: &gelatin::Display,
index: usize,
frame_id: Option<isize>,
) -> PathedTextureResult {
let path = self
.dir
.image_by_index(index)
.ok_or(PathResolutionError::WaitingOnDirFilter)?
.path
.clone();
let result = self.load_specific(display, &path, frame_id);
Ok((path, result))
}
pub fn load_specific(
&mut self,
display: &gelatin::Display,
path: &Path,
frame_id: Option<isize>,
) -> TextureResult<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().ok_or_else(|| {
TextureError::Other("Could not get path for current image".into())
})?;
self.send_request_for_file(path, request_id, RequestKind::Priority { display });
return Err(TextureError::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: &gelatin::Display) -> PathedTextureResult {
self.load_jump(display, 1, 0)
}
pub fn load_prev(&mut self, display: &gelatin::Display) -> PathedTextureResult {
self.load_jump(display, -1, 0)
}
pub fn load_jump(
&mut self,
display: &gelatin::Display,
file_jump_count: i32,
frame_jump_count: isize,
) -> PathedTextureResult {
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 Ok((path, requested));
} else {
return Err(PathResolutionError::NotYetSpecified);
}
} 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 {
log::info!("Folder is empty, no folder was open, or folder hasn't finished filtering when trying to jump to an image by index.");
return Err(PathResolutionError::NotYetSpecified);
}
let result = self.load_specific(display, &target_path, None);
Ok((target_path, result))
}
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: &gelatin::Display) -> TextureResult<()> {
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(e) => {
retval = Err(e);
}
}
}
}
#[allow(clippy::question_mark)]
if retval.is_err() {
return retval;
}
if uploaded_one {
break;
}
}
Ok(())
}
fn try_getting_requested_image(
&mut self,
display: &gelatin::Display,
frame_id: isize,
) -> TextureResult<AnimationFrameTexture> {
trace!("Begin `try_getting_requested_image` in `image_cache`");
let DirItem { path, request_id: req_id } = self
.curr_dir_item()
.ok_or_else(|| TextureError::Other("Could not get path for current image".into()))?;
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(TextureError::from_failed_request(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(TextureError::WaitingOnLoader);
}
if self.pending_requests.contains(&req_id) {
PRIORITY_REQUEST_ID.store(req_id, Ordering::SeqCst);
return Err(TextureError::WaitingOnLoader);
}
self.send_request_for_file(path, req_id, RequestKind::Priority { display });
Err(TextureError::WaitingOnLoader)
}
fn upload_to_texture(
&mut self,
display: &gelatin::Display,
load_result: LoadResult,
) -> TextureResult<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(TextureError::from_failed_request(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 =
self.dir.image_by_index(index).map(|desc| (desc.path.clone(), desc.request_id));
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) -> directory::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,
) -> directory::Result<()> {
self.dir.change_directory_with_filename(dir_path, filename)
}
}
fn get_file_name_and_parent(path: &Path) -> std::io::Result<(OsString, PathBuf)> {
use std::io;
let file_name = match path.file_name() {
Some(f) => f.to_owned(),
None => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("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() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Could not get parent directory of {:?}", path),
));
}
path
}
};
Ok((file_name, parent))
}