How to persist data (textures) between renders #1513
Replies: 3 comments 10 replies
-
Load the texture once, and reuse it. See https://github.com/emilk/egui/blob/0.17.0/eframe/examples/image.rs You don't need to implement |
Beta Was this translation helpful? Give feedback.
-
rather than creating a temporary widget. just use a BTreeMap with the row and column as key. for ((x, y), piece) in board_map.iter() {
ui.add(piece);
} The only problem is texture duplication between various pieces if you use more than one ChessPiece referring to the same texture. to avoid this complexity, if its just a simple chessboard. preload the textures in advance and keep them in a struct or |
Beta Was this translation helpful? Give feedback.
-
@emilk So hey, I'm back. First up, take all of this with a grain of salt, I'm not all that experienced in rust, and although I'm interested in every field, most of my time I spent is developing in languages like TypeScript. And now the "but" lol, I may criticize how the textures are currently handled ^^ :). First of all, this is the API I coded up: APIconst PIECE_PATH: &str = "src/assets/bishop_black.svg";
const PIECE_SIZE: texture_manager::Size = (255, 255);
const PIECE_SIZE_VEC: egui::Vec2 = egui::vec2(PIECE_SIZE.0 as f32, PIECE_SIZE.1 as f32);
// Obviously would be nicer if it were `ui.texture_manager`, but I didn't bother
// modifying the source code. More about this later in "integration" anyway.
let texture_manager = self.texture_manager.get_or_insert_with(|| {
let mut tm = TextureManager::new(ui.ctx().tex_manager());
// User-extensible parsing of files
tm.set_byte_parser_for("svg", SVG_BYTES_PARSER);
tm.set_byte_parser_for("png", png_bytes_parser);
tm
});
// let texture_id = texture_manager.load(PIECE_PATH);
let texture_id = texture_manager.load_sized(PIECE_PATH, &PIECE_SIZE);
ui.image(texture_id, PIECE_SIZE_VEC) User-extensible bytes parser / Flawed SVG parserGiven that a user might want to use all sorts of files, I thought I'd make the parsing extensible, especially because I've found the current SVG parser to be a bit flawed. To be specific, it always used the size specified in the SVG element, which meant, the image would never be able to be reasonable shown bigger than it was defined, which sort of is the point of SVGs as far as I'm aware. Anyway, here is my implementation lol (you might find a lot of similarity with the one from const SVG_BYTES_PARSER: texture_manager::BytesParser = |bytes, size| {
let options = usvg::Options::default();
let tree = usvg::Tree::from_data(&bytes, &options.to_ref()).unwrap();
let (width, height) = match size {
Some(size) => *size,
None => {
let svg_node_size = tree.svg_node().size;
(
svg_node_size.width() as usize,
svg_node_size.height() as usize,
)
}
};
let mut pixmap = tiny_skia::Pixmap::new(width as u32, height as u32).unwrap();
resvg::render(
&tree,
usvg::FitTo::Size(width as u32, height as u32),
tiny_skia::Transform::default(),
pixmap.as_mut(),
)
.unwrap();
egui::ColorImage::from_rgba_unmultiplied([width as _, height as _], pixmap.data())
}; And here the png parser, which is basically a wrapper for fn png_bytes_parser(bytes: Arc<[u8]>, _: Option<&texture_manager::Size>) -> egui::ColorImage {
egui_extras::image::load_image_bytes(&bytes).unwrap()
} IntegrationIn the current state, the API is very "external" from egui, basically wrapping the original texture manager (big part of my criticism to come). In the future, (from a user standpoint) it would be nice if it could be accessed like Existing texture API criticismAgain, take all of this with a grain of salt, I respect your work and don't mean to be rude. First I implemented my texture manager so would cache So I've been asking myself, is there any reason to use those IDs and So currently you can't have a mesh with a texture (which means no image?), without instantiating and maintaining the texture manager and co. ImplementationSo here is the implementation. It's probably not optimal, missing a few lines of documentation, and things like error handing are missing. It was fun to implement and I learnt a lot, but I don't really fancy spending a bunch of extra time if it's not going to be utilized. The current version of it is definitely able to sustain my needs. Feel free to get back to me though, if you want me to do some changes or have suggestions. use eframe::{
egui,
epaint::{ColorImage, TextureId},
};
// TODO: Error handling
use std::{collections::HashMap, sync::Arc};
pub type Path = String;
pub type Size = (usize, usize);
/// Parses given bytes to a color image.
///
/// * `Arc<[u8]>` - bytes to be parsed
/// * [`Option<&Size>`] - the target for the processing - useful when rasterizing
pub type BytesParser = fn(Arc<[u8]>, Option<&Size>) -> ColorImage;
pub struct TextureManager {
egui_texture_manager: Arc<eframe::egui::mutex::RwLock<eframe::epaint::TextureManager>>,
bytes_cache: HashMap<Path, Arc<[u8]>>,
texture_id_cache: HashMap<(Path, Size), TextureId>,
bytes_parsers: HashMap<String, BytesParser>,
}
impl TextureManager {
pub fn new(
egui_texture_manager: Arc<eframe::egui::mutex::RwLock<eframe::epaint::TextureManager>>,
) -> Self {
Self {
egui_texture_manager,
bytes_cache: HashMap::new(),
texture_id_cache: HashMap::new(),
bytes_parsers: HashMap::new(),
}
}
/// Add a new parser for a given file (extension).
pub fn set_byte_parser_for(&mut self, extension: &str, parser: BytesParser) {
self.bytes_parsers.insert(extension.to_owned(), parser);
}
/// Load an image **without** specifying a size. Also see [`Self::load_sized()`].
///
/// **Given that a size is needed for caching, size (0, 0) will be used.**
///
/// The parser will not receive any size, and will be asked to infer it from
/// the files content.
pub fn load(&mut self, path: &str) -> TextureId {
self.internal_load(path, None)
}
/// Load an image and size it accordingly. Also see [`Self::load()`].
///
/// Depending on the filetype and it's parser, the size may be completely ignored.
/// For other parsers, for example SVG, this attribute determines at what size
/// the image will be rasterized.
pub fn load_sized(&mut self, path: &str, size: &Size) -> egui::epaint::TextureId {
self.internal_load(path, Some(size))
}
/// "Completely" unloads the bytes of the image. Also see [`Self::unload_sized()`].
///
/// If a sized version was allocated, it might sill remain in that cache,
/// especially if multiple sizes were requested.
/// If unloading is important, make sure to explicitly unload all (sized) versions
/// with [`Self::unload_sized()`].
pub fn unload(&mut self, path: &str) {
self.bytes_cache.remove(path);
// TODO: figure out if this can be done without cloning.
self.texture_id_cache.remove(&(path.to_owned(), (0, 0)));
}
/// Unloads **only** a particular size of an image. Also see [`Self::unload()`].
///
/// The underlying bytes of the image will remain cached.
pub fn unload_sized(&mut self, path: &str, size: &Size) {
// TODO: figure out if this can be done without cloning.
self.texture_id_cache.remove(&(path.to_owned(), *size));
}
fn internal_load(&mut self, path: &str, size: Option<&Size>) -> TextureId {
// Textures that don't have an associated size will be cached with the
// values (0, 0).
let size_or_zero = *(size.clone()).get_or_insert(&(0, 0));
// TODO: figure out if this can be done without cloning.
if let Some(cached_texture_id) =
self.texture_id_cache.get(&(path.to_owned(), *size_or_zero))
{
return cached_texture_id.clone();
}
// TODO: Error handling
let file_ext = std::path::Path::new(path)
.extension()
.unwrap()
.to_str()
.unwrap();
let bytes = self.get_bytes(path);
// --------------------------------------------------------------------
//
// I think it would be better to just cache this, and render it instead
// of dealing with the weird TextureId / TextureHandle.
//
// --------------------------------------------------------------------
let image = self.parse_bytes(bytes, file_ext, size);
let texture_id = self
.egui_texture_manager
.write()
.alloc(path.to_owned(), image.into());
let texture_id = texture_id;
self.texture_id_cache
.insert((path.to_owned(), *size_or_zero), texture_id.clone());
texture_id
}
fn get_bytes(&mut self, path: &str) -> Arc<[u8]> {
if let Some(cached_bytes) = self.bytes_cache.get(path) {
return cached_bytes.clone();
}
let bytes: Arc<[u8]> = std::fs::read(path).ok().map(|bytes| bytes.into()).unwrap();
self.bytes_cache.insert(path.to_owned(), bytes.clone());
bytes
}
fn parse_bytes(&mut self, bytes: Arc<[u8]>, ext: &str, size: Option<&Size>) -> ColorImage {
let parser = self
.bytes_parsers
.get(ext)
.expect(&format!("no parser registered for extension {}", ext));
parser(bytes, size)
}
} PerformanceI don't really know if this is good performance or not, but here the output of the following code: let now = Instant::now();
let texture_id = texture_manager.load_sized(PIECE_PATH, &PIECE_SIZE);
println!("allocated texture in: {:.2?}", now.elapsed()); |
Beta Was this translation helpful? Give feedback.
-
I'm trying to implement a Widget that renders a certain chess piece. Eventually it should support all of the pieces, but keeping it simple for this.
Currently I'm using this code, which I've seen a bunch:
Now, either I don't know how to correctly add widget to the UI, or I don't know how to correctly persist my texture.
I need to have that widget for every square. Thus I use the following code:
That currently means the struct is re-created every render, which totally freezes the app. Any suggestions?
Note: The position calcu
Beta Was this translation helpful? Give feedback.
All reactions