1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 13:20:05 -04:00

Texture loading in egui (#1110)

* Move texture allocation into epaint/egui proper
* Add TextureHandle
* egui_glow: cast using bytemuck instead of unsafe code
* Optimize glium painter
* Optimize WebGL
* Add example of loading an image from file
This commit is contained in:
Emil Ernerfeldt
2022-01-15 13:59:52 +01:00
committed by GitHub
parent 6c616a1b69
commit 66d80e2519
59 changed files with 1297 additions and 860 deletions

241
epaint/src/image.rs Normal file
View File

@@ -0,0 +1,241 @@
use crate::Color32;
/// An image stored in RAM.
///
/// To load an image file, see [`ColorImage::from_rgba_unmultiplied`].
///
/// In order to paint the image on screen, you first need to convert it to
///
/// See also: [`ColorImage`], [`AlphaImage`].
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageData {
/// RGBA image.
Color(ColorImage),
/// Used for the font texture.
Alpha(AlphaImage),
}
impl ImageData {
pub fn size(&self) -> [usize; 2] {
match self {
Self::Color(image) => image.size,
Self::Alpha(image) => image.size,
}
}
pub fn width(&self) -> usize {
self.size()[0]
}
pub fn height(&self) -> usize {
self.size()[1]
}
pub fn bytes_per_pixel(&self) -> usize {
match self {
Self::Color(_) => 4,
Self::Alpha(_) => 1,
}
}
}
// ----------------------------------------------------------------------------
/// A 2D RGBA color image in RAM.
#[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ColorImage {
/// width, height.
pub size: [usize; 2],
/// The pixels, row by row, from top to bottom.
pub pixels: Vec<Color32>,
}
impl ColorImage {
/// Create an image filled with the given color.
pub fn new(size: [usize; 2], color: Color32) -> Self {
Self {
size,
pixels: vec![color; size[0] * size[1]],
}
}
/// Create an `Image` from flat un-multiplied RGBA data.
///
/// This is usually what you want to use after having loaded an image file.
///
/// Panics if `size[0] * size[1] * 4 != rgba.len()`.
///
/// ## Example using the [`image`](crates.io/crates/image) crate:
/// ``` ignore
/// fn load_image_from_path(path: &std::path::Path) -> Result<egui::ColorImage, image::ImageError> {
/// use image::GenericImageView as _;
/// let image = image::io::Reader::open(path)?.decode()?;
/// let size = [image.width() as _, image.height() as _];
/// let image_buffer = image.to_rgba8();
/// let pixels = image_buffer.as_flat_samples();
/// Ok(egui::ColorImage::from_rgba_unmultiplied(
/// size,
/// pixels.as_slice(),
/// ))
/// }
///
/// fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
/// use image::GenericImageView as _;
/// let image = image::load_from_memory(image_data)?;
/// let size = [image.width() as _, image.height() as _];
/// let image_buffer = image.to_rgba8();
/// let pixels = image_buffer.as_flat_samples();
/// Ok(ColorImage::from_rgba_unmultiplied(
/// size,
/// pixels.as_slice(),
/// ))
/// }
/// ```
pub fn from_rgba_unmultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
assert_eq!(size[0] * size[1] * 4, rgba.len());
let pixels = rgba
.chunks_exact(4)
.map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
.collect();
Self { size, pixels }
}
/// An example color image, useful for tests.
pub fn example() -> Self {
let width = 128;
let height = 64;
let mut img = Self::new([width, height], Color32::TRANSPARENT);
for y in 0..height {
for x in 0..width {
let h = x as f32 / width as f32;
let s = 1.0;
let v = 1.0;
let a = y as f32 / height as f32;
img[(x, y)] = crate::color::Hsva { h, s, v, a }.into();
}
}
img
}
#[inline]
pub fn width(&self) -> usize {
self.size[0]
}
#[inline]
pub fn height(&self) -> usize {
self.size[1]
}
}
impl std::ops::Index<(usize, usize)> for ColorImage {
type Output = Color32;
#[inline]
fn index(&self, (x, y): (usize, usize)) -> &Color32 {
let [w, h] = self.size;
assert!(x < w && y < h);
&self.pixels[y * w + x]
}
}
impl std::ops::IndexMut<(usize, usize)> for ColorImage {
#[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Color32 {
let [w, h] = self.size;
assert!(x < w && y < h);
&mut self.pixels[y * w + x]
}
}
impl From<ColorImage> for ImageData {
#[inline(always)]
fn from(image: ColorImage) -> Self {
Self::Color(image)
}
}
// ----------------------------------------------------------------------------
/// An 8-bit image, representing difference levels of transparent white.
///
/// Used for the font texture
#[derive(Clone, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct AlphaImage {
/// width, height
pub size: [usize; 2],
/// The alpha (linear space 0-255) of something white.
///
/// One byte per pixel. Often you want to use [`Self::srgba_pixels`] instead.
pub pixels: Vec<u8>,
}
impl AlphaImage {
pub fn new(size: [usize; 2]) -> Self {
Self {
size,
pixels: vec![0; size[0] * size[1]],
}
}
#[inline]
pub fn width(&self) -> usize {
self.size[0]
}
#[inline]
pub fn height(&self) -> usize {
self.size[1]
}
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
///
/// `gamma` should normally be set to 1.0.
/// If you are having problems with text looking skinny and pixelated, try
/// setting a lower gamma, e.g. `0.5`.
pub fn srgba_pixels(
&'_ self,
gamma: f32,
) -> impl ExactSizeIterator<Item = super::Color32> + '_ {
let srgba_from_alpha_lut: Vec<Color32> = (0..=255)
.map(|a| {
let a = super::color::linear_f32_from_linear_u8(a).powf(gamma);
super::Rgba::from_white_alpha(a).into()
})
.collect();
self.pixels
.iter()
.map(move |&a| srgba_from_alpha_lut[a as usize])
}
}
impl std::ops::Index<(usize, usize)> for AlphaImage {
type Output = u8;
#[inline]
fn index(&self, (x, y): (usize, usize)) -> &u8 {
let [w, h] = self.size;
assert!(x < w && y < h);
&self.pixels[y * w + x]
}
}
impl std::ops::IndexMut<(usize, usize)> for AlphaImage {
#[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut u8 {
let [w, h] = self.size;
assert!(x < w && y < h);
&mut self.pixels[y * w + x]
}
}
impl From<AlphaImage> for ImageData {
#[inline(always)]
fn from(image: AlphaImage) -> Self {
Self::Alpha(image)
}
}

View File

@@ -88,6 +88,7 @@
#![allow(clippy::manual_range_contains)]
pub mod color;
pub mod image;
mod mesh;
pub mod mutex;
mod shadow;
@@ -98,10 +99,13 @@ mod stroke;
pub mod tessellator;
pub mod text;
mod texture_atlas;
mod texture_handle;
pub mod textures;
pub mod util;
pub use {
color::{Color32, Rgba},
image::{AlphaImage, ColorImage, ImageData},
mesh::{Mesh, Mesh16, Vertex},
shadow::Shadow,
shape::{CircleShape, PathShape, RectShape, Shape, TextShape},
@@ -110,6 +114,8 @@ pub use {
tessellator::{tessellate_shapes, TessellationOptions, Tessellator},
text::{Fonts, Galley, TextStyle},
texture_atlas::{FontImage, TextureAtlas},
texture_handle::TextureHandle,
textures::TextureManager,
};
pub use emath::{pos2, vec2, Pos2, Rect, Vec2};
@@ -124,21 +130,25 @@ pub use emath;
pub const WHITE_UV: emath::Pos2 = emath::pos2(0.0, 0.0);
/// What texture to use in a [`Mesh`] mesh.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
///
/// If you don't want to use a texture, use `TextureId::Epaint(0)` and the [`WHITE_UV`] for uv-coord.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum TextureId {
/// The egui font texture.
/// If you don't want to use a texture, pick this and the [`WHITE_UV`] for uv-coord.
Egui,
/// Textures allocated using [`TextureManager`].
///
/// The first texture (`TextureId::Epaint(0)`) is used for the font data.
Managed(u64),
/// Your own texture, defined in any which way you want.
/// egui won't care. The backend renderer will presumably use this to look up what texture to use.
/// The backend renderer will presumably use this to look up what texture to use.
User(u64),
}
impl Default for TextureId {
/// The epaint font texture.
fn default() -> Self {
Self::Egui
Self::Managed(0)
}
}

View File

@@ -105,7 +105,7 @@ impl Mesh {
#[inline(always)]
pub fn colored_vertex(&mut self, pos: Pos2, color: Color32) {
crate::epaint_assert!(self.texture_id == TextureId::Egui);
crate::epaint_assert!(self.texture_id == TextureId::default());
self.vertices.push(Vertex {
pos,
uv: WHITE_UV,
@@ -168,7 +168,7 @@ impl Mesh {
/// Uniformly colored rectangle.
#[inline(always)]
pub fn add_colored_rect(&mut self, rect: Rect, color: Color32) {
crate::epaint_assert!(self.texture_id == TextureId::Egui);
crate::epaint_assert!(self.texture_id == TextureId::default());
self.add_rect_with_uv(rect, [WHITE_UV, WHITE_UV].into(), color);
}

View File

@@ -149,7 +149,7 @@ impl Shape {
if let Shape::Mesh(mesh) = self {
mesh.texture_id
} else {
super::TextureId::Egui
super::TextureId::default()
}
}

View File

@@ -376,12 +376,12 @@ fn allocate_glyph(
} else {
let glyph_pos = atlas.allocate((glyph_width, glyph_height));
let texture = atlas.image_mut();
let image = atlas.image_mut();
glyph.draw(|x, y, v| {
if v > 0.0 {
let px = glyph_pos.0 + x as usize;
let py = glyph_pos.1 + y as usize;
texture[(px, py)] = (v * 255.0).round() as u8;
image.image[(px, py)] = (v * 255.0).round() as u8;
}
});

View File

@@ -28,7 +28,7 @@ pub enum TextStyle {
}
impl TextStyle {
pub fn all() -> impl Iterator<Item = TextStyle> {
pub fn all() -> impl ExactSizeIterator<Item = TextStyle> {
[
TextStyle::Small,
TextStyle::Body,
@@ -253,13 +253,13 @@ impl Fonts {
// We want an atlas big enough to be able to include all the Emojis in the `TextStyle::Heading`,
// so we can show the Emoji picker demo window.
let mut atlas = TextureAtlas::new(2048, 64);
let mut atlas = TextureAtlas::new([2048, 64]);
{
// Make the top left pixel fully white:
let pos = atlas.allocate((1, 1));
assert_eq!(pos, (0, 0));
atlas.image_mut()[pos] = 255;
atlas.image_mut().image[pos] = 255;
}
let atlas = Arc::new(Mutex::new(atlas));
@@ -287,7 +287,7 @@ impl Fonts {
let mut atlas = atlas.lock();
let texture = atlas.image_mut();
// Make sure we seed the texture version with something unique based on the default characters:
texture.version = crate::util::hash(&texture.pixels);
texture.version = crate::util::hash(&texture.image);
}
Self {
@@ -295,7 +295,7 @@ impl Fonts {
definitions,
fonts,
atlas,
buffered_font_image: Default::default(), //atlas.lock().texture().clone();
buffered_font_image: Default::default(),
galley_cache: Default::default(),
}
}

View File

@@ -1,59 +1,30 @@
use crate::image::AlphaImage;
/// An 8-bit texture containing font data.
#[derive(Clone, Default)]
pub struct FontImage {
/// e.g. a hash of the data. Use this to detect changes!
/// If the texture changes, this too will change.
pub version: u64,
pub width: usize,
pub height: usize,
/// The alpha (linear space 0-255) of something white.
///
/// One byte per pixel. Often you want to use [`Self::srgba_pixels`] instead.
pub pixels: Vec<u8>,
/// The actual image data.
pub image: AlphaImage,
}
impl FontImage {
#[inline]
pub fn size(&self) -> [usize; 2] {
[self.width, self.height]
self.image.size
}
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
///
/// `gamma` should normally be set to 1.0.
/// If you are having problems with egui text looking skinny and pixelated, try
/// setting a lower gamma, e.g. `0.5`.
pub fn srgba_pixels(&'_ self, gamma: f32) -> impl Iterator<Item = super::Color32> + '_ {
use super::Color32;
let srgba_from_luminance_lut: Vec<Color32> = (0..=255)
.map(|a| {
let a = super::color::linear_f32_from_linear_u8(a).powf(gamma);
super::Rgba::from_white_alpha(a).into()
})
.collect();
self.pixels
.iter()
.map(move |&l| srgba_from_luminance_lut[l as usize])
}
}
impl std::ops::Index<(usize, usize)> for FontImage {
type Output = u8;
#[inline]
fn index(&self, (x, y): (usize, usize)) -> &u8 {
assert!(x < self.width);
assert!(y < self.height);
&self.pixels[y * self.width + x]
pub fn width(&self) -> usize {
self.image.size[0]
}
}
impl std::ops::IndexMut<(usize, usize)> for FontImage {
#[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut u8 {
assert!(x < self.width);
assert!(y < self.height);
&mut self.pixels[y * self.width + x]
pub fn height(&self) -> usize {
self.image.size[1]
}
}
@@ -70,13 +41,11 @@ pub struct TextureAtlas {
}
impl TextureAtlas {
pub fn new(width: usize, height: usize) -> Self {
pub fn new(size: [usize; 2]) -> Self {
Self {
image: FontImage {
version: 0,
width,
height,
pixels: vec![0; width * height],
image: AlphaImage::new(size),
},
..Default::default()
}
@@ -99,12 +68,12 @@ impl TextureAtlas {
const PADDING: usize = 1;
assert!(
w <= self.image.width,
w <= self.image.width(),
"Tried to allocate a {} wide glyph in a {} wide texture atlas",
w,
self.image.width
self.image.width()
);
if self.cursor.0 + w > self.image.width {
if self.cursor.0 + w > self.image.width() {
// New row:
self.cursor.0 = 0;
self.cursor.1 += self.row_height + PADDING;
@@ -112,15 +81,7 @@ impl TextureAtlas {
}
self.row_height = self.row_height.max(h);
while self.cursor.1 + self.row_height >= self.image.height {
self.image.height *= 2;
}
if self.image.width * self.image.height > self.image.pixels.len() {
self.image
.pixels
.resize(self.image.width * self.image.height, 0);
}
resize_to_min_height(&mut self.image.image, self.cursor.1 + self.row_height);
let pos = self.cursor;
self.cursor.0 += w + PADDING;
@@ -128,3 +89,13 @@ impl TextureAtlas {
(pos.0 as usize, pos.1 as usize)
}
}
fn resize_to_min_height(image: &mut AlphaImage, min_height: usize) {
while min_height >= image.height() {
image.size[1] *= 2; // double the height
}
if image.width() * image.height() > image.pixels.len() {
image.pixels.resize(image.width() * image.height(), 0);
}
}

View File

@@ -0,0 +1,107 @@
use crate::{
emath::NumExt,
mutex::{Arc, RwLock},
ImageData, TextureId, TextureManager,
};
/// Used to paint images.
///
/// An _image_ is pixels stored in RAM, and represented using [`ImageData`].
/// Before you can paint it however, you need to convert it to a _texture_.
///
/// If you are using egui, use `egui::Context::load_texture`.
///
/// The [`TextureHandle`] can be cloned cheaply.
/// When the last [`TextureHandle`] for specific texture is dropped, the texture is freed.
///
/// See also [`TextureManager`].
#[must_use]
pub struct TextureHandle {
tex_mngr: Arc<RwLock<TextureManager>>,
id: TextureId,
}
impl Drop for TextureHandle {
fn drop(&mut self) {
self.tex_mngr.write().free(self.id);
}
}
impl Clone for TextureHandle {
fn clone(&self) -> Self {
self.tex_mngr.write().retain(self.id);
Self {
tex_mngr: self.tex_mngr.clone(),
id: self.id,
}
}
}
impl PartialEq for TextureHandle {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for TextureHandle {}
impl std::hash::Hash for TextureHandle {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl TextureHandle {
/// If you are using egui, use `egui::Context::load_texture` instead.
pub fn new(tex_mngr: Arc<RwLock<TextureManager>>, id: TextureId) -> Self {
Self { tex_mngr, id }
}
#[inline]
pub fn id(&self) -> TextureId {
self.id
}
/// Assign a new image to an existing texture.
pub fn set(&mut self, image: impl Into<ImageData>) {
self.tex_mngr.write().set(self.id, image.into());
}
/// width x height
pub fn size(&self) -> [usize; 2] {
self.tex_mngr.read().meta(self.id).unwrap().size
}
/// width x height
pub fn size_vec2(&self) -> crate::Vec2 {
let [w, h] = self.size();
crate::Vec2::new(w as f32, h as f32)
}
/// width / height
pub fn aspect_ratio(&self) -> f32 {
let [w, h] = self.size();
w as f32 / h.at_least(1) as f32
}
/// Debug-name.
pub fn name(&self) -> String {
self.tex_mngr.read().meta(self.id).unwrap().name.clone()
}
}
impl From<&TextureHandle> for TextureId {
#[inline(always)]
fn from(handle: &TextureHandle) -> Self {
handle.id()
}
}
impl From<&mut TextureHandle> for TextureId {
#[inline(always)]
fn from(handle: &mut TextureHandle) -> Self {
handle.id()
}
}

161
epaint/src/textures.rs Normal file
View File

@@ -0,0 +1,161 @@
use crate::{image::ImageData, TextureId};
use ahash::AHashMap;
// ----------------------------------------------------------------------------
/// 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: AHashMap<TextureId, TextureMeta>,
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) == TexureId::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) -> 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,
});
self.delta.set.insert(id, image);
id
}
/// Assign a new image to an existing texture.
pub fn set(&mut self, id: TextureId, image: ImageData) {
if let Some(meta) = self.metas.get_mut(&id) {
meta.size = image.size();
meta.bytes_per_pixel = image.bytes_per_pixel();
self.delta.set.insert(id, image);
} else {
crate::epaint_assert!(
false,
"Tried setting texture {:?} which is not allocated",
id
);
}
}
/// 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.push(id);
}
} else {
crate::epaint_assert!(
false,
"Tried freeing texture {:?} which is not allocated",
id
);
}
}
/// 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 {
crate::epaint_assert!(
false,
"Tried retaining texture {:?} which is not allocated",
id
);
}
}
/// Take and reset changes since last frame.
///
/// These should be applied to the painting subsystem each frame.
pub fn take_delta(&mut self) -> TexturesDelta {
std::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<Item = (&TextureId, &TextureMeta)> {
self.metas.iter()
}
/// Total number of allocated textures.
pub fn num_allocated(&self) -> usize {
self.metas.len()
}
}
/// Meta-data about an allocated texture.
#[derive(Clone, Debug, PartialEq)]
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,
}
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
}
}
// ----------------------------------------------------------------------------
/// What has been allocated and freed during the last period.
///
/// These are commands given to the integration painter.
#[derive(Clone, Default, PartialEq)]
#[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: AHashMap<TextureId, ImageData>,
/// Texture to free after painting.
pub free: Vec<TextureId>,
}
impl TexturesDelta {
pub fn append(&mut self, mut newer: TexturesDelta) {
self.set.extend(newer.set.into_iter());
self.free.append(&mut newer.free);
}
}