From 462f181db36496897b9d248795cbb1c30f091069 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 22 Jan 2022 11:23:12 +0100 Subject: [PATCH] Partial font texture update (#1149) --- CHANGELOG.md | 4 +- egui/src/context.rs | 17 ++-- egui/src/introspection.rs | 94 ++++++++++---------- egui/src/widgets/plot/items/mod.rs | 8 +- egui_demo_lib/benches/benchmark.rs | 6 +- egui_demo_lib/src/apps/demo/plot_demo.rs | 5 +- egui_glium/src/epi_backend.rs | 4 +- egui_glium/src/lib.rs | 4 +- egui_glium/src/painter.rs | 26 ++++-- egui_glow/src/epi_backend.rs | 4 +- egui_glow/src/lib.rs | 4 +- egui_glow/src/painter.rs | 67 +++++++++----- egui_web/src/backend.rs | 4 +- egui_web/src/glow_wrapping.rs | 4 +- egui_web/src/painter.rs | 2 +- egui_web/src/webgl1.rs | 76 ++++++++++------ egui_web/src/webgl2.rs | 77 +++++++++------- epaint/src/image.rs | 59 +++++++++++++ epaint/src/lib.rs | 4 +- epaint/src/text/font.rs | 6 +- epaint/src/text/fonts.rs | 34 +++---- epaint/src/texture_atlas.rs | 107 ++++++++++++++--------- epaint/src/texture_handle.rs | 13 ++- epaint/src/textures.rs | 19 ++-- 24 files changed, 393 insertions(+), 255 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3351f20d..7545fa6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,9 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)). * Renamed `Ui::visible` to `Ui::is_visible`. * Split `Event::Text` into `Event::Text` and `Event::Paste` ([#1058](https://github.com/emilk/egui/pull/1058)). -* For integrations: `FontImage` has been replaced by `TexturesDelta` (found in `Output`), describing what textures were loaded and freed each frame ([#1110](https://github.com/emilk/egui/pull/1110)). +* For integrations: + * `FontImage` has been replaced by `TexturesDelta` (found in `Output`), describing what textures were loaded and freed each frame ([#1110](https://github.com/emilk/egui/pull/1110)). + * The painter must support partial texture updates ([#1149](https://github.com/emilk/egui/pull/1149)). ### Fixed 🐛 * Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)) diff --git a/egui/src/context.rs b/egui/src/context.rs index 8b56d26b..a982ebfa 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -33,7 +33,6 @@ struct ContextImpl { fonts: Option, memory: Memory, animation_manager: AnimationManager, - latest_font_image_version: Option, tex_manager: WrappedTextureManager, input: InputState, @@ -709,17 +708,15 @@ impl Context { .memory .end_frame(&ctx_impl.input, &ctx_impl.frame_state.used_ids); - let font_image = ctx_impl.fonts.as_ref().unwrap().font_image(); - let font_image_version = font_image.version; - - if Some(font_image_version) != ctx_impl.latest_font_image_version { + let font_image_delta = ctx_impl.fonts.as_ref().unwrap().font_image_delta(); + if let Some(font_image_delta) = font_image_delta { ctx_impl .tex_manager .0 .write() - .set(TextureId::default(), font_image.image.clone().into()); - ctx_impl.latest_font_image_version = Some(font_image_version); + .set(TextureId::default(), font_image_delta); } + ctx_impl .output .textures_delta @@ -757,7 +754,7 @@ impl Context { let clipped_meshes = tessellator::tessellate_shapes( shapes, tessellation_options, - self.fonts().font_image().size(), + self.fonts().font_image_size(), ); self.write().paint_stats = paint_stats.with_clipped_meshes(&clipped_meshes); clipped_meshes @@ -961,8 +958,8 @@ impl Context { .show(ui, |ui| { let mut font_definitions = self.fonts().definitions().clone(); font_definitions.ui(ui); - let font_image = self.fonts().font_image(); - font_image.ui(ui); + let font_image_size = self.fonts().font_image_size(); + crate::introspection::font_texture_ui(ui, font_image_size); self.set_fonts(font_definitions); }); diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 9ec2a97c..04525119 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -1,60 +1,58 @@ //! uis for egui types. use crate::*; -impl Widget for &epaint::FontImage { - fn ui(self, ui: &mut Ui) -> Response { - use epaint::Mesh; +// Show font texture in demo Ui +pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Response { + use epaint::Mesh; - ui.vertical(|ui| { - // Show font texture in demo Ui - let [width, height] = self.size(); + ui.vertical(|ui| { + let color = if ui.visuals().dark_mode { + Color32::WHITE + } else { + Color32::BLACK + }; - ui.label(format!( - "Texture size: {} x {} (hover to zoom)", - width, height - )); - if width <= 1 || height <= 1 { - return; - } - let mut size = vec2(width as f32, height as f32); - if size.x > ui.available_width() { - size *= ui.available_width() / size.x; - } - let (rect, response) = ui.allocate_at_least(size, Sense::hover()); - let mut mesh = Mesh::default(); - mesh.add_rect_with_uv( - rect, - [pos2(0.0, 0.0), pos2(1.0, 1.0)].into(), - Color32::WHITE, - ); - ui.painter().add(Shape::mesh(mesh)); + ui.label(format!( + "Texture size: {} x {} (hover to zoom)", + width, height + )); + if width <= 1 || height <= 1 { + return; + } + let mut size = vec2(width as f32, height as f32); + if size.x > ui.available_width() { + size *= ui.available_width() / size.x; + } + let (rect, response) = ui.allocate_at_least(size, Sense::hover()); + let mut mesh = Mesh::default(); + mesh.add_rect_with_uv(rect, [pos2(0.0, 0.0), pos2(1.0, 1.0)].into(), color); + ui.painter().add(Shape::mesh(mesh)); - let (tex_w, tex_h) = (width as f32, height as f32); + let (tex_w, tex_h) = (width as f32, height as f32); - response - .on_hover_cursor(CursorIcon::ZoomIn) - .on_hover_ui_at_pointer(|ui| { - if let Some(pos) = ui.ctx().latest_pointer_pos() { - let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0)); - let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w); - let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h); + response + .on_hover_cursor(CursorIcon::ZoomIn) + .on_hover_ui_at_pointer(|ui| { + if let Some(pos) = ui.ctx().latest_pointer_pos() { + let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0)); + let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w); + let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h); - let texel_radius = 32.0; - let u = u.at_least(texel_radius).at_most(tex_w - texel_radius); - let v = v.at_least(texel_radius).at_most(tex_h - texel_radius); + let texel_radius = 32.0; + let u = u.at_least(texel_radius).at_most(tex_w - texel_radius); + let v = v.at_least(texel_radius).at_most(tex_h - texel_radius); - let uv_rect = Rect::from_min_max( - pos2((u - texel_radius) / tex_w, (v - texel_radius) / tex_h), - pos2((u + texel_radius) / tex_w, (v + texel_radius) / tex_h), - ); - let mut mesh = Mesh::default(); - mesh.add_rect_with_uv(zoom_rect, uv_rect, Color32::WHITE); - ui.painter().add(Shape::mesh(mesh)); - } - }); - }) - .response - } + let uv_rect = Rect::from_min_max( + pos2((u - texel_radius) / tex_w, (v - texel_radius) / tex_h), + pos2((u + texel_radius) / tex_w, (v + texel_radius) / tex_h), + ); + let mut mesh = Mesh::default(); + mesh.add_rect_with_uv(zoom_rect, uv_rect, color); + ui.painter().add(Shape::mesh(mesh)); + } + }); + }) + .response } impl Widget for &mut epaint::text::FontDefinitions { diff --git a/egui/src/widgets/plot/items/mod.rs b/egui/src/widgets/plot/items/mod.rs index 99f7fb86..12c00ae3 100644 --- a/egui/src/widgets/plot/items/mod.rs +++ b/egui/src/widgets/plot/items/mod.rs @@ -1087,9 +1087,13 @@ pub struct PlotImage { impl PlotImage { /// Create a new image with position and size in plot coordinates. - pub fn new(texture_id: impl Into, position: Value, size: impl Into) -> Self { + pub fn new( + texture_id: impl Into, + center_position: Value, + size: impl Into, + ) -> Self { Self { - position, + position: center_position, name: Default::default(), highlight: false, texture_id: texture_id.into(), diff --git a/egui_demo_lib/benches/benchmark.rs b/egui_demo_lib/benches/benchmark.rs index 8d62e858..d1d549bd 100644 --- a/egui_demo_lib/benches/benchmark.rs +++ b/egui_demo_lib/benches/benchmark.rs @@ -114,11 +114,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let text_shape = TextShape::new(egui::Pos2::ZERO, galley); c.bench_function("tessellate_text", |b| { b.iter(|| { - tessellator.tessellate_text( - fonts.font_image().size(), - text_shape.clone(), - &mut mesh, - ); + tessellator.tessellate_text(fonts.font_image_size(), text_shape.clone(), &mut mesh); mesh.clear(); }) }); diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 00f34ab1..a172c218 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -343,10 +343,7 @@ impl Widget for &mut ItemsDemo { let image = PlotImage::new( texture, Value::new(0.0, 10.0), - [ - ui.fonts().font_image().width() as f32 / 100.0, - ui.fonts().font_image().height() as f32 / 100.0, - ], + 5.0 * vec2(texture.aspect_ratio(), 1.0), ); let plot = Plot::new("items_demo") diff --git a/egui_glium/src/epi_backend.rs b/egui_glium/src/epi_backend.rs index 3ad93b5b..73ae74d3 100644 --- a/egui_glium/src/epi_backend.rs +++ b/egui_glium/src/epi_backend.rs @@ -70,8 +70,8 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { integration.update(display.gl_window().window()); let clipped_meshes = integration.egui_ctx.tessellate(shapes); - for (id, image) in textures_delta.set { - painter.set_texture(&display, id, &image); + for (id, image_delta) in textures_delta.set { + painter.set_texture(&display, id, &image_delta); } // paint: diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index b8b7c647..c43578dd 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -155,8 +155,8 @@ impl EguiGlium { let shapes = std::mem::take(&mut self.shapes); let mut textures_delta = std::mem::take(&mut self.textures_delta); - for (id, image) in textures_delta.set { - self.painter.set_texture(display, id, &image); + for (id, image_delta) in textures_delta.set { + self.painter.set_texture(display, id, &image_delta); } let clipped_meshes = self.egui_ctx.tessellate(shapes); diff --git a/egui_glium/src/painter.rs b/egui_glium/src/painter.rs index f003db04..384b74da 100644 --- a/egui_glium/src/painter.rs +++ b/egui_glium/src/painter.rs @@ -185,9 +185,9 @@ impl Painter { &mut self, facade: &dyn glium::backend::Facade, tex_id: egui::TextureId, - image: &egui::ImageData, + delta: &egui::epaint::ImageDelta, ) { - let pixels: Vec<(u8, u8, u8, u8)> = match image { + let pixels: Vec<(u8, u8, u8, u8)> = match &delta.image { egui::ImageData::Color(image) => { assert_eq!( image.width() * image.height(), @@ -206,15 +206,29 @@ impl Painter { }; let glium_image = glium::texture::RawImage2d { data: std::borrow::Cow::Owned(pixels), - width: image.width() as _, - height: image.height() as _, + width: delta.image.width() as _, + height: delta.image.height() as _, format: glium::texture::ClientFormat::U8U8U8U8, }; let format = texture::SrgbFormat::U8U8U8U8; let mipmaps = texture::MipmapsOption::NoMipmap; - let gl_texture = SrgbTexture2d::with_format(facade, glium_image, format, mipmaps).unwrap(); - self.textures.insert(tex_id, gl_texture.into()); + if let Some(pos) = delta.pos { + // update a sub-region + if let Some(gl_texture) = self.textures.get(&tex_id) { + let rect = glium::Rect { + left: pos[0] as _, + bottom: pos[1] as _, + width: glium_image.width, + height: glium_image.height, + }; + gl_texture.main_level().write(rect, glium_image); + } + } else { + let gl_texture = + SrgbTexture2d::with_format(facade, glium_image, format, mipmaps).unwrap(); + self.textures.insert(tex_id, gl_texture.into()); + } } pub fn free_texture(&mut self, tex_id: egui::TextureId) { diff --git a/egui_glow/src/epi_backend.rs b/egui_glow/src/epi_backend.rs index 8b6c6011..86ad9d66 100644 --- a/egui_glow/src/epi_backend.rs +++ b/egui_glow/src/epi_backend.rs @@ -86,8 +86,8 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { integration.update(gl_window.window()); let clipped_meshes = integration.egui_ctx.tessellate(shapes); - for (id, image) in textures_delta.set { - painter.set_texture(&gl, id, &image); + for (id, image_delta) in textures_delta.set { + painter.set_texture(&gl, id, &image_delta); } // paint: diff --git a/egui_glow/src/lib.rs b/egui_glow/src/lib.rs index 113c48f2..8e150b88 100644 --- a/egui_glow/src/lib.rs +++ b/egui_glow/src/lib.rs @@ -175,8 +175,8 @@ impl EguiGlow { let shapes = std::mem::take(&mut self.shapes); let mut textures_delta = std::mem::take(&mut self.textures_delta); - for (id, image) in textures_delta.set { - self.painter.set_texture(gl, id, &image); + for (id, image_delta) in textures_delta.set { + self.painter.set_texture(gl, id, &image_delta); } let clipped_meshes = self.egui_ctx.tessellate(shapes); diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index b0eb2c87..baa97869 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -340,6 +340,7 @@ impl Painter { gl.bind_texture(glow::TEXTURE_2D, Some(texture)); } + // Transform clip rect to physical pixels: let clip_min_x = pixels_per_point * clip_rect.min.x; let clip_min_y = pixels_per_point * clip_rect.min.y; @@ -386,7 +387,7 @@ impl Painter { &mut self, gl: &glow::Context, tex_id: egui::TextureId, - image: &egui::ImageData, + delta: &egui::epaint::ImageDelta, ) { self.assert_not_destroyed(); @@ -398,7 +399,7 @@ impl Painter { gl.bind_texture(glow::TEXTURE_2D, Some(glow_texture)); } - match image { + match &delta.image { egui::ImageData::Color(image) => { assert_eq!( image.width() * image.height(), @@ -408,7 +409,7 @@ impl Painter { let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref()); - self.upload_texture_srgb(gl, image.size, data); + self.upload_texture_srgb(gl, delta.pos, image.size, data); } egui::ImageData::Alpha(image) => { assert_eq!( @@ -427,12 +428,18 @@ impl Painter { .flat_map(|a| a.to_array()) .collect(); - self.upload_texture_srgb(gl, image.size, &data); + self.upload_texture_srgb(gl, delta.pos, image.size, &data); } }; } - fn upload_texture_srgb(&mut self, gl: &glow::Context, [w, h]: [usize; 2], data: &[u8]) { + fn upload_texture_srgb( + &mut self, + gl: &glow::Context, + pos: Option<[usize; 2]>, + [w, h]: [usize; 2], + data: &[u8], + ) { assert_eq!(data.len(), w * h * 4); assert!(w >= 1 && h >= 1); unsafe { @@ -457,38 +464,50 @@ impl Painter { glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32, ); + check_for_gl_error(gl, "tex_parameter"); - if self.is_webgl_1 { + let (internal_format, src_format) = if self.is_webgl_1 { let format = if self.srgb_support { glow::SRGB_ALPHA } else { glow::RGBA }; - gl.tex_image_2d( - glow::TEXTURE_2D, - 0, - format as i32, - w as i32, - h as i32, - 0, - format, - glow::UNSIGNED_BYTE, - Some(data), - ); + (format, format) } else { + (glow::SRGB8_ALPHA8, glow::RGBA) + }; + + gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1); + + let level = 0; + if let Some([x, y]) = pos { + gl.tex_sub_image_2d( + glow::TEXTURE_2D, + level, + x as _, + y as _, + w as _, + h as _, + src_format, + glow::UNSIGNED_BYTE, + glow::PixelUnpackData::Slice(data), + ); + check_for_gl_error(gl, "tex_sub_image_2d"); + } else { + let border = 0; gl.tex_image_2d( glow::TEXTURE_2D, - 0, - glow::SRGB8_ALPHA8 as i32, - w as i32, - h as i32, - 0, - glow::RGBA, + level, + internal_format as _, + w as _, + h as _, + border, + src_format, glow::UNSIGNED_BYTE, Some(data), ); + check_for_gl_error(gl, "tex_image_2d"); } - check_for_gl_error(gl, "upload_texture_srgb"); } } diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index f6692e76..7158ef55 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -221,8 +221,8 @@ impl AppRunner { /// Paint the results of the last call to [`Self::logic`]. pub fn paint(&mut self, clipped_meshes: Vec) -> Result<(), JsValue> { let textures_delta = std::mem::take(&mut self.textures_delta); - for (id, image) in textures_delta.set { - self.painter.set_texture(id, image); + for (id, image_delta) in textures_delta.set { + self.painter.set_texture(id, &image_delta); } self.painter.clear(self.app.clear_color()); diff --git a/egui_web/src/glow_wrapping.rs b/egui_web/src/glow_wrapping.rs index 28337dc9..35ab6de2 100644 --- a/egui_web/src/glow_wrapping.rs +++ b/egui_web/src/glow_wrapping.rs @@ -40,8 +40,8 @@ impl WrappedGlowPainter { } impl crate::Painter for WrappedGlowPainter { - fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) { - self.painter.set_texture(&self.glow_ctx, tex_id, &image); + fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { + self.painter.set_texture(&self.glow_ctx, tex_id, delta); } fn free_texture(&mut self, tex_id: egui::TextureId) { diff --git a/egui_web/src/painter.rs b/egui_web/src/painter.rs index 8c246a4c..2688d2a5 100644 --- a/egui_web/src/painter.rs +++ b/egui_web/src/painter.rs @@ -1,7 +1,7 @@ use wasm_bindgen::prelude::JsValue; pub trait Painter { - fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData); + fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta); fn free_texture(&mut self, tex_id: egui::TextureId); diff --git a/egui_web/src/webgl1.rs b/egui_web/src/webgl1.rs index 83617fa4..973cec78 100644 --- a/egui_web/src/webgl1.rs +++ b/egui_web/src/webgl1.rs @@ -40,13 +40,6 @@ impl WebGlPainter { // -------------------------------------------------------------------- - let egui_texture = gl.create_texture().unwrap(); - gl.bind_texture(Gl::TEXTURE_2D, Some(&egui_texture)); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as i32); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32); - let srgb_supported = matches!(gl.get_extension("EXT_sRGB"), Ok(Some(_))); let vert_shader = compile_shader( @@ -222,36 +215,61 @@ impl WebGlPainter { Ok(()) } - fn set_texture_rgba(&mut self, tex_id: egui::TextureId, size: [usize; 2], pixels: &[u8]) { + fn set_texture_rgba( + &mut self, + tex_id: egui::TextureId, + pos: Option<[usize; 2]>, + [w, h]: [usize; 2], + pixels: &[u8], + ) { let gl = &self.gl; - let gl_texture = gl.create_texture().unwrap(); - gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); + + let gl_texture = self + .textures + .entry(tex_id) + .or_insert_with(|| gl.create_texture().unwrap()); + + gl.bind_texture(Gl::TEXTURE_2D, Some(gl_texture)); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as _); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as _); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as _); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as _); - gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); - let level = 0; let internal_format = self.texture_format; let border = 0; let src_format = self.texture_format; let src_type = Gl::UNSIGNED_BYTE; - gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( - Gl::TEXTURE_2D, - level, - internal_format as _, - size[0] as _, - size[1] as _, - border, - src_format, - src_type, - Some(pixels), - ) - .unwrap(); - self.textures.insert(tex_id, gl_texture); + gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1); + + if let Some([x, y]) = pos { + gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + level, + x as _, + y as _, + w as _, + h as _, + src_format, + src_type, + Some(pixels), + ) + .unwrap(); + } else { + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + level, + internal_format as _, + w as _, + h as _, + border, + src_format, + src_type, + Some(pixels), + ) + .unwrap(); + } } } @@ -271,8 +289,8 @@ impl epi::NativeTexture for WebGlPainter { } impl crate::Painter for WebGlPainter { - fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) { - match image { + fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { + match &delta.image { egui::ImageData::Color(image) => { assert_eq!( image.width() * image.height(), @@ -281,7 +299,7 @@ impl crate::Painter for WebGlPainter { ); let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref()); - self.set_texture_rgba(tex_id, image.size, data); + self.set_texture_rgba(tex_id, delta.pos, image.size, data); } egui::ImageData::Alpha(image) => { let gamma = if self.post_process.is_none() { @@ -293,7 +311,7 @@ impl crate::Painter for WebGlPainter { .srgba_pixels(gamma) .flat_map(|a| a.to_array()) .collect(); - self.set_texture_rgba(tex_id, image.size, &data); + self.set_texture_rgba(tex_id, delta.pos, image.size, &data); } }; } diff --git a/egui_web/src/webgl2.rs b/egui_web/src/webgl2.rs index 8d0d5cca..778ff109 100644 --- a/egui_web/src/webgl2.rs +++ b/egui_web/src/webgl2.rs @@ -40,13 +40,6 @@ impl WebGl2Painter { // -------------------------------------------------------------------- - let egui_texture = gl.create_texture().unwrap(); - gl.bind_texture(Gl::TEXTURE_2D, Some(&egui_texture)); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as i32); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32); - let vert_shader = compile_shader( &gl, Gl::VERTEX_SHADER, @@ -206,37 +199,61 @@ impl WebGl2Painter { Ok(()) } - fn set_texture_rgba(&mut self, tex_id: egui::TextureId, size: [usize; 2], pixels: &[u8]) { + fn set_texture_rgba( + &mut self, + tex_id: egui::TextureId, + pos: Option<[usize; 2]>, + [w, h]: [usize; 2], + pixels: &[u8], + ) { let gl = &self.gl; - let gl_texture = gl.create_texture().unwrap(); - gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); + + let gl_texture = self + .textures + .entry(tex_id) + .or_insert_with(|| gl.create_texture().unwrap()); + + gl.bind_texture(Gl::TEXTURE_2D, Some(gl_texture)); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as _); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as _); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as _); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as _); - gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); - let level = 0; let internal_format = Gl::SRGB8_ALPHA8; let border = 0; let src_format = Gl::RGBA; let src_type = Gl::UNSIGNED_BYTE; - gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1); - gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( - Gl::TEXTURE_2D, - level, - internal_format as i32, - size[0] as i32, - size[1] as i32, - border, - src_format, - src_type, - Some(pixels), - ) - .unwrap(); - self.textures.insert(tex_id, gl_texture); + gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1); + + if let Some([x, y]) = pos { + gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + level, + x as _, + y as _, + w as _, + h as _, + src_format, + src_type, + Some(pixels), + ) + .unwrap(); + } else { + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + level, + internal_format as _, + w as _, + h as _, + border, + src_format, + src_type, + Some(pixels), + ) + .unwrap(); + } } } @@ -256,8 +273,8 @@ impl epi::NativeTexture for WebGl2Painter { } impl crate::Painter for WebGl2Painter { - fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) { - match image { + fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { + match &delta.image { egui::ImageData::Color(image) => { assert_eq!( image.width() * image.height(), @@ -266,7 +283,7 @@ impl crate::Painter for WebGl2Painter { ); let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref()); - self.set_texture_rgba(tex_id, image.size, data); + self.set_texture_rgba(tex_id, delta.pos, image.size, data); } egui::ImageData::Alpha(image) => { let gamma = 1.0; @@ -274,7 +291,7 @@ impl crate::Painter for WebGl2Painter { .srgba_pixels(gamma) .flat_map(|a| a.to_array()) .collect(); - self.set_texture_rgba(tex_id, image.size, &data); + self.set_texture_rgba(tex_id, delta.pos, image.size, &data); } }; } diff --git a/epaint/src/image.rs b/epaint/src/image.rs index 67a4d846..bbf3a228 100644 --- a/epaint/src/image.rs +++ b/epaint/src/image.rs @@ -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 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) -> Self { + Self { + image: image.into(), + pos: None, + } + } + + /// Update a sub-region of an existing texture. + pub fn partial(pos: [usize; 2], image: impl Into) -> 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() + } +} diff --git a/epaint/src/lib.rs b/epaint/src/lib.rs index 547d5340..07d34d1f 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -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, }; diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index 6c180ff2..f0eaaa97 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -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; } }); diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index 0cd36e00..c363a8d7 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -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, atlas: Arc>, - - /// 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>, - galley_cache: Mutex, } @@ -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 { - 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 { + 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. diff --git a/epaint/src/texture_atlas.rs b/epaint/src/texture_atlas.rs index 08c6a067..8e5e1c52 100644 --- a/epaint/src/texture_atlas.rs +++ b/epaint/src/texture_atlas.rs @@ -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 { + 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 } } diff --git a/epaint/src/texture_handle.rs b/epaint/src/texture_handle.rs index 8cb37941..fd10de26 100644 --- a/epaint/src/texture_handle.rs +++ b/epaint/src/texture_handle.rs @@ -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) { - 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) { + self.tex_mngr + .write() + .set(self.id, ImageDelta::partial(pos, image.into())); } /// width x height diff --git a/epaint/src/textures.rs b/epaint/src/textures.rs index 79ef685f..a67382f0 100644 --- a/epaint/src/textures.rs +++ b/epaint/src/textures.rs @@ -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, + pub set: AHashMap, /// Texture to free after painting. pub free: Vec,