Skip to main content

epaint/
textures.rs

1use crate::{ImageData, ImageDelta, TextureId};
2use ahash::{HashMap, HashSet};
3use smallvec::{SmallVec, smallvec};
4use std::mem;
5
6// ----------------------------------------------------------------------------
7
8/// Low-level manager for allocating textures.
9///
10/// Communicates with the painting subsystem using [`Self::take_delta`].
11#[derive(Default)]
12pub struct TextureManager {
13    /// We allocate texture id:s linearly.
14    next_id: u64,
15
16    /// Information about currently allocated textures.
17    metas: ahash::HashMap<TextureId, TextureMeta>,
18
19    delta: TexturesDelta,
20}
21
22impl TextureManager {
23    /// Allocate a new texture.
24    ///
25    /// The given name can be useful for later debugging.
26    ///
27    /// The returned [`TextureId`] will be [`TextureId::Managed`], with an index
28    /// starting from zero and increasing with each call to [`Self::alloc`].
29    ///
30    /// The first texture you allocate will be `TextureId::Managed(0) == TextureId::default()` and
31    /// MUST have a white pixel at (0,0) ([`crate::WHITE_UV`]).
32    ///
33    /// The texture is given a retain-count of `1`, requiring one call to [`Self::free`] to free it.
34    pub fn alloc(&mut self, name: String, image: ImageData, options: TextureOptions) -> TextureId {
35        let id = TextureId::Managed(self.next_id);
36        self.next_id += 1;
37
38        self.metas.entry(id).or_insert_with(|| TextureMeta {
39            name,
40            size: image.size(),
41            bytes_per_pixel: image.bytes_per_pixel(),
42            retain_count: 1,
43            options,
44        });
45
46        self.delta.push(id, ImageDelta::full(image, options));
47        id
48    }
49
50    /// Assign a new image to an existing texture,
51    /// or update a region of it.
52    pub fn set(&mut self, id: TextureId, delta: ImageDelta) {
53        if let Some(meta) = self.metas.get_mut(&id) {
54            if let Some(pos) = delta.pos {
55                debug_assert!(
56                    pos[0] + delta.image.width() <= meta.size[0]
57                        && pos[1] + delta.image.height() <= meta.size[1],
58                    "Partial texture update is outside the bounds of texture {id:?}",
59                );
60            } else {
61                // whole update
62                meta.size = delta.image.size();
63                meta.bytes_per_pixel = delta.image.bytes_per_pixel();
64            }
65            self.delta.push(id, delta);
66        } else {
67            debug_assert!(false, "Tried setting texture {id:?} which is not allocated");
68        }
69    }
70
71    /// Free an existing texture.
72    pub fn free(&mut self, id: TextureId) {
73        if let std::collections::hash_map::Entry::Occupied(mut entry) = self.metas.entry(id) {
74            let meta = entry.get_mut();
75            meta.retain_count -= 1;
76            if meta.retain_count == 0 {
77                entry.remove();
78                self.delta.free(id);
79            }
80        } else {
81            debug_assert!(false, "Tried freeing texture {id:?} which is not allocated");
82        }
83    }
84
85    /// Increase the retain-count of the given texture.
86    ///
87    /// For each time you call [`Self::retain`] you must call [`Self::free`] on additional time.
88    pub fn retain(&mut self, id: TextureId) {
89        if let Some(meta) = self.metas.get_mut(&id) {
90            meta.retain_count += 1;
91        } else {
92            debug_assert!(
93                false,
94                "Tried retaining texture {id:?} which is not allocated",
95            );
96        }
97    }
98
99    /// Take and reset changes since last frame.
100    ///
101    /// These should be applied to the painting subsystem each frame.
102    pub fn take_delta(&mut self) -> TexturesDelta {
103        std::mem::take(&mut self.delta)
104    }
105
106    /// Get meta-data about a specific texture.
107    pub fn meta(&self, id: TextureId) -> Option<&TextureMeta> {
108        self.metas.get(&id)
109    }
110
111    /// Get meta-data about all allocated textures in some arbitrary order.
112    pub fn allocated(&self) -> impl ExactSizeIterator<Item = (&TextureId, &TextureMeta)> {
113        self.metas.iter()
114    }
115
116    /// Total number of allocated textures.
117    pub fn num_allocated(&self) -> usize {
118        self.metas.len()
119    }
120}
121
122impl Drop for TextureManager {
123    fn drop(&mut self) {
124        self.delta.clear(); // Prevent a debug panic on application shutdown
125    }
126}
127
128/// Meta-data about an allocated texture.
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct TextureMeta {
131    /// A human-readable name useful for debugging.
132    pub name: String,
133
134    /// width x height
135    pub size: [usize; 2],
136
137    /// 4 or 1
138    pub bytes_per_pixel: usize,
139
140    /// Free when this reaches zero.
141    pub retain_count: usize,
142
143    /// The texture filtering mode to use when rendering.
144    pub options: TextureOptions,
145}
146
147impl TextureMeta {
148    /// Size in bytes.
149    /// width x height x [`Self::bytes_per_pixel`].
150    pub fn bytes_used(&self) -> usize {
151        self.size[0] * self.size[1] * self.bytes_per_pixel
152    }
153}
154
155// ----------------------------------------------------------------------------
156
157/// How the texture texels are filtered.
158#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
159#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
160pub struct TextureOptions {
161    /// How to filter when magnifying (when texels are larger than pixels).
162    pub magnification: TextureFilter,
163
164    /// How to filter when minifying (when texels are smaller than pixels).
165    pub minification: TextureFilter,
166
167    /// How to wrap the texture when the texture coordinates are outside the [0, 1] range.
168    pub wrap_mode: TextureWrapMode,
169
170    /// How to filter between texture mipmaps.
171    ///
172    /// Mipmaps ensures textures look smooth even when the texture is very small and pixels are much
173    /// larger than individual texels.
174    ///
175    /// # Notes
176    ///
177    /// - This may not be available on all backends (currently only `egui_glow`).
178    pub mipmap_mode: Option<TextureFilter>,
179}
180
181impl TextureOptions {
182    /// Linear magnification and minification.
183    pub const LINEAR: Self = Self {
184        magnification: TextureFilter::Linear,
185        minification: TextureFilter::Linear,
186        wrap_mode: TextureWrapMode::ClampToEdge,
187        mipmap_mode: None,
188    };
189
190    /// Nearest magnification and minification.
191    pub const NEAREST: Self = Self {
192        magnification: TextureFilter::Nearest,
193        minification: TextureFilter::Nearest,
194        wrap_mode: TextureWrapMode::ClampToEdge,
195        mipmap_mode: None,
196    };
197
198    /// Linear magnification and minification, but with the texture repeated.
199    pub const LINEAR_REPEAT: Self = Self {
200        magnification: TextureFilter::Linear,
201        minification: TextureFilter::Linear,
202        wrap_mode: TextureWrapMode::Repeat,
203        mipmap_mode: None,
204    };
205
206    /// Linear magnification and minification, but with the texture mirrored and repeated.
207    pub const LINEAR_MIRRORED_REPEAT: Self = Self {
208        magnification: TextureFilter::Linear,
209        minification: TextureFilter::Linear,
210        wrap_mode: TextureWrapMode::MirroredRepeat,
211        mipmap_mode: None,
212    };
213
214    /// Nearest magnification and minification, but with the texture repeated.
215    pub const NEAREST_REPEAT: Self = Self {
216        magnification: TextureFilter::Nearest,
217        minification: TextureFilter::Nearest,
218        wrap_mode: TextureWrapMode::Repeat,
219        mipmap_mode: None,
220    };
221
222    /// Nearest magnification and minification, but with the texture mirrored and repeated.
223    pub const NEAREST_MIRRORED_REPEAT: Self = Self {
224        magnification: TextureFilter::Nearest,
225        minification: TextureFilter::Nearest,
226        wrap_mode: TextureWrapMode::MirroredRepeat,
227        mipmap_mode: None,
228    };
229
230    pub const fn with_mipmap_mode(self, mipmap_mode: Option<TextureFilter>) -> Self {
231        Self {
232            mipmap_mode,
233            ..self
234        }
235    }
236}
237
238impl Default for TextureOptions {
239    /// The default is linear for both magnification and minification.
240    fn default() -> Self {
241        Self::LINEAR
242    }
243}
244
245/// How the texture texels are filtered.
246#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
247#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
248pub enum TextureFilter {
249    /// Show the nearest pixel value.
250    ///
251    /// When zooming in you will get sharp, square pixels/texels.
252    /// When zooming out you will get a very crisp (and aliased) look.
253    Nearest,
254
255    /// Linearly interpolate the nearest neighbors, creating a smoother look when zooming in and out.
256    Linear,
257}
258
259/// Defines how textures are wrapped around objects when texture coordinates fall outside the [0, 1] range.
260#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
261#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
262pub enum TextureWrapMode {
263    /// Stretches the edge pixels to fill beyond the texture's bounds.
264    ///
265    /// This is what you want to use for a normal image in a GUI.
266    #[default]
267    ClampToEdge,
268
269    /// Tiles the texture across the surface, repeating it horizontally and vertically.
270    Repeat,
271
272    /// Mirrors the texture with each repetition, creating symmetrical tiling.
273    MirroredRepeat,
274}
275
276// ----------------------------------------------------------------------------
277
278/// What has been allocated and freed during the last period.
279///
280/// These are commands given to the integration painter.
281#[derive(Clone, Default, PartialEq, Eq)]
282#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
283#[must_use = "The painter must take care of this"]
284pub struct TexturesDelta {
285    /// New or changed textures. Apply before painting.
286    pub set: HashMap<TextureId, SmallVec<[ImageDelta; 1]>>,
287
288    /// Textures to free after painting.
289    pub free: HashSet<TextureId>,
290}
291
292impl TexturesDelta {
293    pub fn is_empty(&self) -> bool {
294        self.set.is_empty() && self.free.is_empty()
295    }
296
297    /// Inserts a [`ImageDelta`].
298    ///
299    /// If this [`TexturesDelta`] already contains this [`TextureId`], and this is a `whole` delta,
300    /// the previous deltas for this id are discarded.
301    pub fn push(&mut self, id: TextureId, delta: ImageDelta) {
302        if delta.is_whole() {
303            // It replaces the whole texture, fine to overwrite any previous deltas
304            self.set.insert(id, smallvec![delta]);
305        } else {
306            self.set.entry(id).or_default().push(delta);
307        }
308    }
309
310    pub fn free(&mut self, id: TextureId) {
311        self.free.insert(id);
312    }
313
314    #[expect(clippy::iter_over_hash_type)]
315    pub fn append(&mut self, mut newer: Self) {
316        // Only clear previous entries on append, not on set, since within a frame a texture might
317        // be created and immediately removed again.
318        for id in &newer.free {
319            self.set.remove(id);
320        }
321        for (id, deltas) in newer.set.drain() {
322            for delta in deltas {
323                self.push(id, delta);
324            }
325        }
326        self.free.extend(mem::take(&mut newer.free));
327    }
328
329    pub fn clear(&mut self) {
330        self.set.clear();
331        self.free.clear();
332    }
333}
334
335impl Drop for TexturesDelta {
336    fn drop(&mut self) {
337        debug_assert!(
338            self.is_empty(),
339            "Dropped TexturesDelta with {} unapplied deltas. Deltas need to be handled. \
340            If you want to drop this intentionally call `clear` before dropping.",
341            self.free.len() + self.set.len()
342        );
343    }
344}
345
346impl std::fmt::Debug for TexturesDelta {
347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        use std::fmt::Write as _;
349
350        let mut debug_struct = f.debug_struct("TexturesDelta");
351        if !self.set.is_empty() {
352            let mut string = String::new();
353            #[expect(clippy::iter_over_hash_type)]
354            for (tex_id, deltas) in &self.set {
355                for delta in deltas {
356                    let size = delta.image.size();
357                    if let Some(pos) = delta.pos {
358                        write!(
359                            string,
360                            "{:?} partial ([{} {}] - [{} {}]), ",
361                            tex_id,
362                            pos[0],
363                            pos[1],
364                            pos[0] + size[0],
365                            pos[1] + size[1]
366                        )
367                        .ok();
368                    } else {
369                        write!(string, "{:?} full {}x{}, ", tex_id, size[0], size[1]).ok();
370                    }
371                }
372            }
373            debug_struct.field("set", &string);
374        }
375        if !self.free.is_empty() {
376            debug_struct.field("free", &self.free);
377        }
378        debug_struct.finish()
379    }
380}