mirror of
https://github.com/emilk/egui.git
synced 2026-06-28 07:23:13 -04:00
Partial font texture update (#1149)
This commit is contained in:
@@ -211,6 +211,23 @@ impl AlphaImage {
|
||||
.iter()
|
||||
.map(move |&a| srgba_from_alpha_lut[a as usize])
|
||||
}
|
||||
|
||||
/// Clone a sub-region as a new image
|
||||
pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> AlphaImage {
|
||||
assert!(x + w <= self.width());
|
||||
assert!(y + h <= self.height());
|
||||
|
||||
let mut pixels = Vec::with_capacity(w * h);
|
||||
for y in y..y + h {
|
||||
let offset = y * self.width() + x;
|
||||
pixels.extend(&self.pixels[offset..(offset + w)]);
|
||||
}
|
||||
assert_eq!(pixels.len(), w * h);
|
||||
AlphaImage {
|
||||
size: [w, h],
|
||||
pixels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<(usize, usize)> for AlphaImage {
|
||||
@@ -239,3 +256,45 @@ impl From<AlphaImage> for ImageData {
|
||||
Self::Alpha(image)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A change to an image.
|
||||
///
|
||||
/// Either a whole new image,
|
||||
/// or an update to a rectangular region of it.
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[must_use = "The painter must take care of this"]
|
||||
pub struct ImageDelta {
|
||||
/// What to set the texture to.
|
||||
pub image: ImageData,
|
||||
|
||||
/// If `None`, set the whole texture to [`Self::image`].
|
||||
/// If `Some(pos)`, update a sub-region of an already allocated texture.
|
||||
pub pos: Option<[usize; 2]>,
|
||||
}
|
||||
|
||||
impl ImageDelta {
|
||||
/// Update the whole texture.
|
||||
pub fn full(image: impl Into<ImageData>) -> Self {
|
||||
Self {
|
||||
image: image.into(),
|
||||
pos: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a sub-region of an existing texture.
|
||||
pub fn partial(pos: [usize; 2], image: impl Into<ImageData>) -> Self {
|
||||
Self {
|
||||
image: image.into(),
|
||||
pos: Some(pos),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this affecting the whole texture?
|
||||
/// If `false`, this is a partial (sub-region) update.
|
||||
pub fn is_whole(&self) -> bool {
|
||||
self.pos.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ pub mod util;
|
||||
|
||||
pub use {
|
||||
color::{Color32, Rgba},
|
||||
image::{AlphaImage, ColorImage, ImageData},
|
||||
image::{AlphaImage, ColorImage, ImageData, ImageDelta},
|
||||
mesh::{Mesh, Mesh16, Vertex},
|
||||
shadow::Shadow,
|
||||
shape::{CircleShape, PathShape, RectShape, Shape, TextShape},
|
||||
@@ -113,7 +113,7 @@ pub use {
|
||||
stroke::Stroke,
|
||||
tessellator::{tessellate_shapes, TessellationOptions, Tessellator},
|
||||
text::{Fonts, Galley, TextStyle},
|
||||
texture_atlas::{FontImage, TextureAtlas},
|
||||
texture_atlas::TextureAtlas,
|
||||
texture_handle::TextureHandle,
|
||||
textures::TextureManager,
|
||||
};
|
||||
|
||||
@@ -374,14 +374,12 @@ fn allocate_glyph(
|
||||
if glyph_width == 0 || glyph_height == 0 {
|
||||
UvRect::default()
|
||||
} else {
|
||||
let glyph_pos = atlas.allocate((glyph_width, glyph_height));
|
||||
|
||||
let image = atlas.image_mut();
|
||||
let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
|
||||
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;
|
||||
image.image[(px, py)] = (v * 255.0).round() as u8;
|
||||
image[(px, py)] = (v * 255.0).round() as u8;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
font::{Font, FontImpl},
|
||||
Galley, LayoutJob,
|
||||
},
|
||||
FontImage, TextureAtlas,
|
||||
TextureAtlas,
|
||||
};
|
||||
|
||||
// TODO: rename
|
||||
@@ -233,11 +233,6 @@ pub struct Fonts {
|
||||
definitions: FontDefinitions,
|
||||
fonts: BTreeMap<TextStyle, Font>,
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
|
||||
/// Copy of the font image in the texture atlas.
|
||||
/// This is so we can return a reference to it (the texture atlas is behind a lock).
|
||||
buffered_font_image: Mutex<Arc<FontImage>>,
|
||||
|
||||
galley_cache: Mutex<GalleyCache>,
|
||||
}
|
||||
|
||||
@@ -257,9 +252,9 @@ impl Fonts {
|
||||
|
||||
{
|
||||
// Make the top left pixel fully white:
|
||||
let pos = atlas.allocate((1, 1));
|
||||
let (pos, image) = atlas.allocate((1, 1));
|
||||
assert_eq!(pos, (0, 0));
|
||||
atlas.image_mut().image[pos] = 255;
|
||||
image[pos] = 255;
|
||||
}
|
||||
|
||||
let atlas = Arc::new(Mutex::new(atlas));
|
||||
@@ -283,19 +278,11 @@ impl Fonts {
|
||||
})
|
||||
.collect();
|
||||
|
||||
{
|
||||
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.image);
|
||||
}
|
||||
|
||||
Self {
|
||||
pixels_per_point,
|
||||
definitions,
|
||||
fonts,
|
||||
atlas,
|
||||
buffered_font_image: Default::default(),
|
||||
galley_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -319,15 +306,14 @@ impl Fonts {
|
||||
(point * self.pixels_per_point).floor() / self.pixels_per_point
|
||||
}
|
||||
|
||||
/// Call each frame to get the latest available font texture data.
|
||||
pub fn font_image(&self) -> Arc<FontImage> {
|
||||
let atlas = self.atlas.lock();
|
||||
let mut buffered_texture = self.buffered_font_image.lock();
|
||||
if buffered_texture.version != atlas.image().version {
|
||||
*buffered_texture = Arc::new(atlas.image().clone());
|
||||
}
|
||||
/// Call each frame to get the change to the font texture since last call.
|
||||
pub fn font_image_delta(&self) -> Option<crate::ImageDelta> {
|
||||
self.atlas.lock().take_delta()
|
||||
}
|
||||
|
||||
buffered_texture.clone()
|
||||
/// Current size of the font image
|
||||
pub fn font_image_size(&self) -> [usize; 2] {
|
||||
self.atlas.lock().size()
|
||||
}
|
||||
|
||||
/// Width of this character in points.
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
use crate::image::AlphaImage;
|
||||
use crate::{AlphaImage, ImageDelta};
|
||||
|
||||
/// 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,
|
||||
|
||||
/// The actual image data.
|
||||
pub image: AlphaImage,
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
struct Rectu {
|
||||
/// inclusive
|
||||
min_x: usize,
|
||||
/// inclusive
|
||||
min_y: usize,
|
||||
/// exclusive
|
||||
max_x: usize,
|
||||
/// exclusive
|
||||
max_y: usize,
|
||||
}
|
||||
|
||||
impl FontImage {
|
||||
#[inline]
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.image.size
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn width(&self) -> usize {
|
||||
self.image.size[0]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> usize {
|
||||
self.image.size[1]
|
||||
}
|
||||
impl Rectu {
|
||||
const NOTHING: Self = Self {
|
||||
min_x: usize::MAX,
|
||||
min_y: usize::MAX,
|
||||
max_x: 0,
|
||||
max_y: 0,
|
||||
};
|
||||
const EVERYTHING: Self = Self {
|
||||
min_x: 0,
|
||||
min_y: 0,
|
||||
max_x: usize::MAX,
|
||||
max_y: usize::MAX,
|
||||
};
|
||||
}
|
||||
|
||||
/// Contains font data in an atlas, where each character occupied a small rectangle.
|
||||
///
|
||||
/// More characters can be added, possibly expanding the texture.
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub struct TextureAtlas {
|
||||
image: FontImage,
|
||||
image: AlphaImage,
|
||||
/// What part of the image that is dirty
|
||||
dirty: Rectu,
|
||||
|
||||
/// Used for when allocating new rectangles.
|
||||
cursor: (usize, usize),
|
||||
@@ -43,25 +44,35 @@ pub struct TextureAtlas {
|
||||
impl TextureAtlas {
|
||||
pub fn new(size: [usize; 2]) -> Self {
|
||||
Self {
|
||||
image: FontImage {
|
||||
version: 0,
|
||||
image: AlphaImage::new(size),
|
||||
},
|
||||
..Default::default()
|
||||
image: AlphaImage::new(size),
|
||||
dirty: Rectu::EVERYTHING,
|
||||
cursor: (0, 0),
|
||||
row_height: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image(&self) -> &FontImage {
|
||||
&self.image
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.image.size
|
||||
}
|
||||
|
||||
pub fn image_mut(&mut self) -> &mut FontImage {
|
||||
self.image.version += 1;
|
||||
&mut self.image
|
||||
/// Call to get the change to the image since last call.
|
||||
pub fn take_delta(&mut self) -> Option<ImageDelta> {
|
||||
let dirty = std::mem::replace(&mut self.dirty, Rectu::NOTHING);
|
||||
if dirty == Rectu::NOTHING {
|
||||
None
|
||||
} else if dirty == Rectu::EVERYTHING {
|
||||
Some(ImageDelta::full(self.image.clone()))
|
||||
} else {
|
||||
let pos = [dirty.min_x, dirty.min_y];
|
||||
let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y];
|
||||
let region = self.image.region(pos, size);
|
||||
Some(ImageDelta::partial(pos, region))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the coordinates of where the rect ended up.
|
||||
pub fn allocate(&mut self, (w, h): (usize, usize)) -> (usize, usize) {
|
||||
/// Returns the coordinates of where the rect ended up,
|
||||
/// and invalidates the region.
|
||||
pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut AlphaImage) {
|
||||
/// On some low-precision GPUs (my old iPad) characters get muddled up
|
||||
/// if we don't add some empty pixels between the characters.
|
||||
/// On modern high-precision GPUs this is not needed.
|
||||
@@ -81,21 +92,31 @@ impl TextureAtlas {
|
||||
}
|
||||
|
||||
self.row_height = self.row_height.max(h);
|
||||
resize_to_min_height(&mut self.image.image, self.cursor.1 + self.row_height);
|
||||
if resize_to_min_height(&mut self.image, self.cursor.1 + self.row_height) {
|
||||
self.dirty = Rectu::EVERYTHING;
|
||||
}
|
||||
|
||||
let pos = self.cursor;
|
||||
self.cursor.0 += w + PADDING;
|
||||
self.image.version += 1;
|
||||
(pos.0 as usize, pos.1 as usize)
|
||||
|
||||
self.dirty.min_x = self.dirty.min_x.min(pos.0);
|
||||
self.dirty.min_y = self.dirty.min_y.min(pos.1);
|
||||
self.dirty.max_x = self.dirty.max_x.max(pos.0 + w);
|
||||
self.dirty.max_y = self.dirty.max_y.max(pos.1 + h);
|
||||
|
||||
(pos, &mut self.image)
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_to_min_height(image: &mut AlphaImage, min_height: usize) {
|
||||
fn resize_to_min_height(image: &mut AlphaImage, min_height: usize) -> bool {
|
||||
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);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
emath::NumExt,
|
||||
mutex::{Arc, RwLock},
|
||||
ImageData, TextureId, TextureManager,
|
||||
ImageData, ImageDelta, TextureId, TextureManager,
|
||||
};
|
||||
|
||||
/// Used to paint images.
|
||||
@@ -66,7 +66,16 @@ impl TextureHandle {
|
||||
|
||||
/// 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());
|
||||
self.tex_mngr
|
||||
.write()
|
||||
.set(self.id, ImageDelta::full(image.into()));
|
||||
}
|
||||
|
||||
/// Assign a new image to a subregion of the whole texture.
|
||||
pub fn set_partial(&mut self, pos: [usize; 2], image: impl Into<ImageData>) {
|
||||
self.tex_mngr
|
||||
.write()
|
||||
.set(self.id, ImageDelta::partial(pos, image.into()));
|
||||
}
|
||||
|
||||
/// width x height
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{image::ImageData, TextureId};
|
||||
use crate::{ImageData, ImageDelta, TextureId};
|
||||
use ahash::AHashMap;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -38,16 +38,19 @@ impl TextureManager {
|
||||
retain_count: 1,
|
||||
});
|
||||
|
||||
self.delta.set.insert(id, image);
|
||||
self.delta.set.insert(id, ImageDelta::full(image));
|
||||
id
|
||||
}
|
||||
|
||||
/// Assign a new image to an existing texture.
|
||||
pub fn set(&mut self, id: TextureId, image: ImageData) {
|
||||
/// 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) {
|
||||
meta.size = image.size();
|
||||
meta.bytes_per_pixel = image.bytes_per_pixel();
|
||||
self.delta.set.insert(id, image);
|
||||
if delta.is_whole() {
|
||||
meta.size = delta.image.size();
|
||||
meta.bytes_per_pixel = delta.image.bytes_per_pixel();
|
||||
}
|
||||
self.delta.set.insert(id, delta);
|
||||
} else {
|
||||
crate::epaint_assert!(
|
||||
false,
|
||||
@@ -147,7 +150,7 @@ impl TextureMeta {
|
||||
#[must_use = "The painter must take care of this"]
|
||||
pub struct TexturesDelta {
|
||||
/// New or changed textures. Apply before painting.
|
||||
pub set: AHashMap<TextureId, ImageData>,
|
||||
pub set: AHashMap<TextureId, ImageDelta>,
|
||||
|
||||
/// Texture to free after painting.
|
||||
pub free: Vec<TextureId>,
|
||||
|
||||
Reference in New Issue
Block a user