use crate::{ImageData, ImageDelta, TextureId}; use ahash::{HashMap, HashSet}; use core::mem; use smallvec::{SmallVec, smallvec}; // ---------------------------------------------------------------------------- /// Low-level manager for allocating textures. /// /// Communicates with the painting subsystem using [`Self::take_delta`]. #[derive(Default)] pub struct TextureManager { /// We allocate texture id:s linearly. next_id: u64, /// Information about currently allocated textures. metas: ahash::HashMap, delta: TexturesDelta, } impl TextureManager { /// Allocate a new texture. /// /// The given name can be useful for later debugging. /// /// The returned [`TextureId`] will be [`TextureId::Managed`], with an index /// starting from zero and increasing with each call to [`Self::alloc`]. /// /// The first texture you allocate will be `TextureId::Managed(0) == TextureId::default()` and /// MUST have a white pixel at (0,0) ([`crate::WHITE_UV`]). /// /// The texture is given a retain-count of `1`, requiring one call to [`Self::free`] to free it. pub fn alloc(&mut self, name: String, image: ImageData, options: TextureOptions) -> TextureId { let id = TextureId::Managed(self.next_id); self.next_id += 1; self.metas.entry(id).or_insert_with(|| TextureMeta { name, size: image.size(), bytes_per_pixel: image.bytes_per_pixel(), retain_count: 1, options, }); self.delta.push(id, ImageDelta::full(image, options)); id } /// Assign a new image to an existing texture, /// or update a region of it. pub fn set(&mut self, id: TextureId, delta: ImageDelta) { if let Some(meta) = self.metas.get_mut(&id) { if let Some(pos) = delta.pos { debug_assert!( pos[0] + delta.image.width() <= meta.size[0] && pos[1] + delta.image.height() <= meta.size[1], "Partial texture update is outside the bounds of texture {id:?}", ); } else { // whole update meta.size = delta.image.size(); meta.bytes_per_pixel = delta.image.bytes_per_pixel(); } self.delta.push(id, delta); } else { debug_assert!(false, "Tried setting texture {id:?} which is not allocated"); } } /// Free an existing texture. pub fn free(&mut self, id: TextureId) { if let std::collections::hash_map::Entry::Occupied(mut entry) = self.metas.entry(id) { let meta = entry.get_mut(); meta.retain_count -= 1; if meta.retain_count == 0 { entry.remove(); self.delta.free(id); } } else { debug_assert!(false, "Tried freeing texture {id:?} which is not allocated"); } } /// Increase the retain-count of the given texture. /// /// For each time you call [`Self::retain`] you must call [`Self::free`] on additional time. pub fn retain(&mut self, id: TextureId) { if let Some(meta) = self.metas.get_mut(&id) { meta.retain_count += 1; } else { debug_assert!( false, "Tried retaining texture {id:?} which is not allocated", ); } } /// Take and reset changes since last frame. /// /// These should be applied to the painting subsystem each frame. pub fn take_delta(&mut self) -> TexturesDelta { core::mem::take(&mut self.delta) } /// Get meta-data about a specific texture. pub fn meta(&self, id: TextureId) -> Option<&TextureMeta> { self.metas.get(&id) } /// Get meta-data about all allocated textures in some arbitrary order. pub fn allocated(&self) -> impl ExactSizeIterator { self.metas.iter() } /// Total number of allocated textures. pub fn num_allocated(&self) -> usize { self.metas.len() } } impl Drop for TextureManager { fn drop(&mut self) { self.delta.clear(); // Prevent a debug panic on application shutdown } } /// Meta-data about an allocated texture. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TextureMeta { /// A human-readable name useful for debugging. pub name: String, /// width x height pub size: [usize; 2], /// 4 or 1 pub bytes_per_pixel: usize, /// Free when this reaches zero. pub retain_count: usize, /// The texture filtering mode to use when rendering. pub options: TextureOptions, } impl TextureMeta { /// Size in bytes. /// width x height x [`Self::bytes_per_pixel`]. pub fn bytes_used(&self) -> usize { self.size[0] * self.size[1] * self.bytes_per_pixel } } // ---------------------------------------------------------------------------- /// How the texture texels are filtered. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct TextureOptions { /// How to filter when magnifying (when texels are larger than pixels). pub magnification: TextureFilter, /// How to filter when minifying (when texels are smaller than pixels). pub minification: TextureFilter, /// How to wrap the texture when the texture coordinates are outside the [0, 1] range. pub wrap_mode: TextureWrapMode, /// How to filter between texture mipmaps. /// /// Mipmaps ensures textures look smooth even when the texture is very small and pixels are much /// larger than individual texels. /// /// # Notes /// /// - This may not be available on all backends (currently only `egui_glow`). pub mipmap_mode: Option, } impl TextureOptions { /// Linear magnification and minification. pub const LINEAR: Self = Self { magnification: TextureFilter::Linear, minification: TextureFilter::Linear, wrap_mode: TextureWrapMode::ClampToEdge, mipmap_mode: None, }; /// Nearest magnification and minification. pub const NEAREST: Self = Self { magnification: TextureFilter::Nearest, minification: TextureFilter::Nearest, wrap_mode: TextureWrapMode::ClampToEdge, mipmap_mode: None, }; /// Linear magnification and minification, but with the texture repeated. pub const LINEAR_REPEAT: Self = Self { magnification: TextureFilter::Linear, minification: TextureFilter::Linear, wrap_mode: TextureWrapMode::Repeat, mipmap_mode: None, }; /// Linear magnification and minification, but with the texture mirrored and repeated. pub const LINEAR_MIRRORED_REPEAT: Self = Self { magnification: TextureFilter::Linear, minification: TextureFilter::Linear, wrap_mode: TextureWrapMode::MirroredRepeat, mipmap_mode: None, }; /// Nearest magnification and minification, but with the texture repeated. pub const NEAREST_REPEAT: Self = Self { magnification: TextureFilter::Nearest, minification: TextureFilter::Nearest, wrap_mode: TextureWrapMode::Repeat, mipmap_mode: None, }; /// Nearest magnification and minification, but with the texture mirrored and repeated. pub const NEAREST_MIRRORED_REPEAT: Self = Self { magnification: TextureFilter::Nearest, minification: TextureFilter::Nearest, wrap_mode: TextureWrapMode::MirroredRepeat, mipmap_mode: None, }; pub const fn with_mipmap_mode(self, mipmap_mode: Option) -> Self { Self { mipmap_mode, ..self } } } impl Default for TextureOptions { /// The default is linear for both magnification and minification. fn default() -> Self { Self::LINEAR } } /// How the texture texels are filtered. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum TextureFilter { /// Show the nearest pixel value. /// /// When zooming in you will get sharp, square pixels/texels. /// When zooming out you will get a very crisp (and aliased) look. Nearest, /// Linearly interpolate the nearest neighbors, creating a smoother look when zooming in and out. Linear, } /// Defines how textures are wrapped around objects when texture coordinates fall outside the [0, 1] range. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum TextureWrapMode { /// Stretches the edge pixels to fill beyond the texture's bounds. /// /// This is what you want to use for a normal image in a GUI. #[default] ClampToEdge, /// Tiles the texture across the surface, repeating it horizontally and vertically. Repeat, /// Mirrors the texture with each repetition, creating symmetrical tiling. MirroredRepeat, } // ---------------------------------------------------------------------------- /// What has been allocated and freed during the last period. /// /// These are commands given to the integration painter. #[derive(Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[must_use = "The painter must take care of this"] pub struct TexturesDelta { /// New or changed textures. Apply before painting. pub set: HashMap>, /// Textures to free after painting. pub free: HashSet, } impl TexturesDelta { pub fn is_empty(&self) -> bool { self.set.is_empty() && self.free.is_empty() } /// Inserts a [`ImageDelta`]. /// /// If this [`TexturesDelta`] already contains this [`TextureId`], and this is a `whole` delta, /// the previous deltas for this id are discarded. pub fn push(&mut self, id: TextureId, delta: ImageDelta) { if delta.is_whole() { // It replaces the whole texture, fine to overwrite any previous deltas self.set.insert(id, smallvec![delta]); } else { self.set.entry(id).or_default().push(delta); } } pub fn free(&mut self, id: TextureId) { self.free.insert(id); } #[expect(clippy::iter_over_hash_type)] pub fn append(&mut self, mut newer: Self) { // Only clear previous entries on append, not on set, since within a frame a texture might // be created and immediately removed again. for id in &newer.free { self.set.remove(id); } for (id, deltas) in newer.set.drain() { for delta in deltas { self.push(id, delta); } } self.free.extend(mem::take(&mut newer.free)); } pub fn clear(&mut self) { self.set.clear(); self.free.clear(); } } impl Drop for TexturesDelta { fn drop(&mut self) { debug_assert!( self.is_empty(), "Dropped TexturesDelta with {} unapplied deltas. Deltas need to be handled. \ If you want to drop this intentionally call `clear` before dropping.", self.free.len() + self.set.len() ); } } impl core::fmt::Debug for TexturesDelta { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { use core::fmt::Write as _; let mut debug_struct = f.debug_struct("TexturesDelta"); if !self.set.is_empty() { let mut string = String::new(); #[expect(clippy::iter_over_hash_type)] for (tex_id, deltas) in &self.set { for delta in deltas { let size = delta.image.size(); if let Some(pos) = delta.pos { write!( string, "{:?} partial ([{} {}] - [{} {}]), ", tex_id, pos[0], pos[1], pos[0] + size[0], pos[1] + size[1] ) .ok(); } else { write!(string, "{:?} full {}x{}, ", tex_id, size[0], size[1]).ok(); } } } debug_struct.field("set", &string); } if !self.free.is_empty() { debug_struct.field("free", &self.free); } debug_struct.finish() } }