Skip to content

Commit

Permalink
refactor(image cache): 🔥 remove TextureLoader and add BytesLoader ins…
Browse files Browse the repository at this point in the history
…tead
  • Loading branch information
melody-rs committed Dec 2, 2023
1 parent 3501004 commit 0358b0a
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 145 deletions.
3 changes: 3 additions & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub struct UpdateState<'res> {
pub graphics: Arc<luminol_graphics::GraphicsState>,
pub filesystem: &'res mut luminol_filesystem::project::FileSystem, // FIXME: this is probably wrong
pub data: &'res mut Data, // FIXME: this is also probably wrong
pub bytes_loader: Arc<luminol_filesystem::egui_bytes_loader::Loader>,

// TODO: look into std::any?
// we're using generics here to allow specialization on the type of window
Expand Down Expand Up @@ -90,6 +91,7 @@ impl<'res> UpdateState<'res> {
graphics: self.graphics.clone(),
filesystem: self.filesystem,
data: self.data,
bytes_loader: self.bytes_loader.clone(),
edit_tabs: self.edit_tabs,
edit_windows,
toasts: self.toasts,
Expand All @@ -108,6 +110,7 @@ impl<'res> UpdateState<'res> {
graphics: self.graphics.clone(),
filesystem: self.filesystem,
data: self.data,
bytes_loader: self.bytes_loader.clone(),
edit_tabs,
edit_windows: self.edit_windows,
toasts: self.toasts,
Expand Down
117 changes: 117 additions & 0 deletions crates/filesystem/src/egui_bytes_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (C) 2023 Lily Lyons
//
// This file is part of Luminol.
//
// Luminol is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Luminol is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Luminol. If not, see <http://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this Program, or any covered work, by linking or combining
// it with Steamworks API by Valve Corporation, containing parts covered by
// terms of the Steamworks API by Valve Corporation, the licensors of this
// Program grant you additional permission to convey the resulting work.

use std::sync::Arc;

use dashmap::{DashMap, DashSet};
use egui::load::{Bytes, BytesPoll, LoadError};

use crate::Error;

#[derive(Default)]
pub struct Loader {
loaded_files: DashMap<camino::Utf8PathBuf, Arc<[u8]>>,
errored_files: DashMap<camino::Utf8PathBuf, Error>,
unloaded_files: DashSet<camino::Utf8PathBuf>,
}

pub const BYTES_LOADER_ID: &str = egui::load::generate_loader_id!(BytesLoader);

pub const PROTOCOL: &str = "project://";

fn supported_uri_to_path(uri: &str) -> Option<&camino::Utf8Path> {
uri.strip_prefix(PROTOCOL).map(camino::Utf8Path::new)
}

impl Loader {
pub fn new() -> Self {
Self::default()
}

pub fn load_unloaded_files(&self, ctx: &egui::Context, filesystem: &impl crate::FileSystem) {
// dashmap has no drain iterator unfortunately so this is the best we can do
for path in self.unloaded_files.iter() {
let path = path.to_path_buf();
match filesystem.read(&path) {
Ok(bytes) => {
self.loaded_files.insert(path, bytes.into());
}
Err(err) => {
self.errored_files.insert(path, err);
}
};
}
if !self.unloaded_files.is_empty() {
ctx.request_repaint();
}
self.unloaded_files.clear();
}
}

impl egui::load::BytesLoader for Loader {
fn id(&self) -> &str {
BYTES_LOADER_ID
}

fn load(&self, _: &egui::Context, uri: &str) -> egui::load::BytesLoadResult {
let Some(path) = supported_uri_to_path(uri) else {
return Err(LoadError::NotSupported);
};

if let Some(bytes) = self.loaded_files.get(path) {
return Ok(BytesPoll::Ready {
size: None,
bytes: Bytes::Shared(bytes.clone()),
mime: None,
});
}

if let Some(error) = self.errored_files.get(path) {
return Err(LoadError::Loading(error.to_string()));
}

self.unloaded_files.insert(path.to_path_buf());
Ok(BytesPoll::Pending { size: None })
}

fn forget(&self, uri: &str) {
let Some(path) = supported_uri_to_path(uri) else {
return;
};

self.loaded_files.remove(path);
self.errored_files.remove(path);
self.unloaded_files.remove(path);
}

fn forget_all(&self) {
self.loaded_files.clear();
self.errored_files.clear();
self.unloaded_files.clear();
}

fn byte_size(&self) -> usize {
self.loaded_files.iter().map(|e| e.len()).sum()
}
}
1 change: 1 addition & 0 deletions crates/filesystem/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// along with Luminol. If not, see <http://www.gnu.org/licenses/>.

pub mod archiver;
pub mod egui_bytes_loader;
pub mod erased;
pub mod list;
pub mod path_cache;
Expand Down
143 changes: 6 additions & 137 deletions crates/graphics/src/texture_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,14 @@
// terms of the Steamworks API by Valve Corporation, the licensors of this
// Program grant you additional permission to convey the resulting work.

use dashmap::{DashMap, DashSet};

use egui::load::{LoadError, SizedTexture, TextureLoadResult, TexturePoll};
use dashmap::DashMap;

use std::sync::Arc;

use wgpu::util::DeviceExt;

pub struct TextureLoader {
// todo: add a load state enum for loading textures (waiting on file -> file read -> image loaded -> texture loaded)
loaded_textures: DashMap<camino::Utf8PathBuf, Arc<Texture>>,
load_errors: DashMap<camino::Utf8PathBuf, anyhow::Error>,
unloaded_textures: DashSet<camino::Utf8PathBuf>,

render_state: egui_wgpu::RenderState,
}
Expand All @@ -54,18 +49,6 @@ impl Drop for Texture {
}
}

pub const TEXTURE_LOADER_ID: &str = egui::load::generate_loader_id!(TextureLoader);

pub const PROTOCOL: &str = "project://";

// NOTE blindly assumes texture components are 1 byte
fn texture_size_bytes(texture: &wgpu::Texture) -> u32 {
texture.width()
* texture.height()
* texture.depth_or_array_layers()
* texture.format().components() as u32
}

fn load_wgpu_texture_from_path(
filesystem: &impl luminol_filesystem::FileSystem,
device: &wgpu::Device,
Expand Down Expand Up @@ -97,15 +80,6 @@ fn load_wgpu_texture_from_path(
))
}

fn supported_uri_to_path(uri: &str) -> Option<&camino::Utf8Path> {
uri.strip_prefix(PROTOCOL)
.map(camino::Utf8Path::new)
.filter(|path| {
let extension = path.extension();
extension.is_some_and(|ext| ext != "svg") || extension.is_none()
})
}

impl Texture {
pub fn size(&self) -> wgpu::Extent3d {
self.texture.size()
Expand All @@ -128,64 +102,11 @@ impl TextureLoader {
pub fn new(render_state: egui_wgpu::RenderState) -> Self {
Self {
loaded_textures: DashMap::with_capacity(64),
load_errors: DashMap::new(),
unloaded_textures: DashSet::with_capacity(64),

render_state,
}
}

pub fn load_unloaded_textures(
&self,
ctx: &egui::Context,
filesystem: &impl luminol_filesystem::FileSystem,
) {
// dashmap has no drain method so this is the best we can do
let mut renderer = self.render_state.renderer.write();
for path in self.unloaded_textures.iter() {
let texture = match load_wgpu_texture_from_path(
filesystem,
&self.render_state.device,
&self.render_state.queue,
path.as_str(),
) {
Ok(t) => t,
Err(error) => {
self.load_errors.insert(path.clone(), error);

continue;
}
};
let view = texture.create_view(&wgpu::TextureViewDescriptor {
label: Some(path.as_str()),
..Default::default()
});

let texture_id = renderer.register_native_texture(
&self.render_state.device,
&view,
wgpu::FilterMode::Nearest,
);

self.loaded_textures.insert(
path.clone(),
Arc::new(Texture {
texture,
view,
texture_id,

render_state: self.render_state.clone(),
}),
);
}

if !self.unloaded_textures.is_empty() {
ctx.request_repaint(); // if we've loaded textures
}

self.unloaded_textures.clear();
}

pub fn load_now_dir(
&self,
filesystem: &impl luminol_filesystem::FileSystem,
Expand Down Expand Up @@ -247,66 +168,14 @@ impl TextureLoader {
pub fn get(&self, path: impl AsRef<camino::Utf8Path>) -> Option<Arc<Texture>> {
self.loaded_textures.get(path.as_ref()).as_deref().cloned()
}
}

impl egui::load::TextureLoader for TextureLoader {
fn id(&self) -> &str {
TEXTURE_LOADER_ID
}

fn load(
&self,
_: &egui::Context,
uri: &str,
_: egui::TextureOptions,
_: egui::SizeHint,
) -> TextureLoadResult {
// check if the uri is supported (starts with project:// and does not end with ".svg")
let Some(path) = supported_uri_to_path(uri) else {
return Err(LoadError::NotSupported);
};

if let Some(texture) = self.loaded_textures.get(path).as_deref() {
return Ok(TexturePoll::Ready {
texture: SizedTexture::new(texture.texture_id, texture.size_vec2()),
});
}

// if during loading we errored, check if it's because the image crate doesn't support loading this file format
if let Some(error) = self.load_errors.get(path) {
match error.downcast_ref::<image::ImageError>() {
Some(image::ImageError::Decoding(error))
if matches!(error.format_hint(), image::error::ImageFormatHint::Unknown) =>
{
return Err(LoadError::NotSupported)
}
Some(image::ImageError::Unsupported(_)) => return Err(LoadError::NotSupported),
_ => return Err(LoadError::Loading(error.to_string())),
}
}

self.unloaded_textures.insert(path.to_path_buf());

Ok(TexturePoll::Pending { size: None })
}

fn forget(&self, uri: &str) {
let Some(path) = supported_uri_to_path(uri) else {
return;
};

self.loaded_textures.remove(path);
pub fn remove(&self, path: impl AsRef<camino::Utf8Path>) -> Option<Arc<Texture>> {
self.loaded_textures
.remove(path.as_ref())
.map(|(_, value)| value)
}

fn forget_all(&self) {
pub fn clear(&self) {
self.loaded_textures.clear();
self.load_errors.clear();
}

fn byte_size(&self) -> usize {
self.loaded_textures
.iter()
.map(|texture| texture_size_bytes(&texture.texture) as usize)
.sum()
}
}
5 changes: 3 additions & 2 deletions crates/ui/src/windows/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// terms of the Steamworks API by Valve Corporation, the licensors of this
// Program grant you additional permission to convey the resulting work.

use egui::load::TextureLoader;
use egui::load::BytesLoader;
use strum::IntoEnumIterator;

#[derive(Default)]
Expand Down Expand Up @@ -97,8 +97,9 @@ impl luminol_core::Window for Window {
)
.clicked()
{
update_state.graphics.texture_loader.forget_all();
update_state.graphics.texture_loader.clear();
update_state.graphics.atlas_cache.clear();
update_state.bytes_loader.forget_all();
}
});
}
Expand Down
Loading

0 comments on commit 0358b0a

Please sign in to comment.