Partial font texture update (#1149)

This commit is contained in:
Emil Ernerfeldt 2022-01-22 11:23:12 +01:00 committed by GitHub
parent 343f7da564
commit 462f181db3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 393 additions and 255 deletions

View file

@ -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)). * `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)).
* Renamed `Ui::visible` to `Ui::is_visible`. * Renamed `Ui::visible` to `Ui::is_visible`.
* Split `Event::Text` into `Event::Text` and `Event::Paste` ([#1058](https://github.com/emilk/egui/pull/1058)). * 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 🐛 ### Fixed 🐛
* Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)) * Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043))

View file

@ -33,7 +33,6 @@ struct ContextImpl {
fonts: Option<Fonts>, fonts: Option<Fonts>,
memory: Memory, memory: Memory,
animation_manager: AnimationManager, animation_manager: AnimationManager,
latest_font_image_version: Option<u64>,
tex_manager: WrappedTextureManager, tex_manager: WrappedTextureManager,
input: InputState, input: InputState,
@ -709,17 +708,15 @@ impl Context {
.memory .memory
.end_frame(&ctx_impl.input, &ctx_impl.frame_state.used_ids); .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_delta = ctx_impl.fonts.as_ref().unwrap().font_image_delta();
let font_image_version = font_image.version; if let Some(font_image_delta) = font_image_delta {
if Some(font_image_version) != ctx_impl.latest_font_image_version {
ctx_impl ctx_impl
.tex_manager .tex_manager
.0 .0
.write() .write()
.set(TextureId::default(), font_image.image.clone().into()); .set(TextureId::default(), font_image_delta);
ctx_impl.latest_font_image_version = Some(font_image_version);
} }
ctx_impl ctx_impl
.output .output
.textures_delta .textures_delta
@ -757,7 +754,7 @@ impl Context {
let clipped_meshes = tessellator::tessellate_shapes( let clipped_meshes = tessellator::tessellate_shapes(
shapes, shapes,
tessellation_options, tessellation_options,
self.fonts().font_image().size(), self.fonts().font_image_size(),
); );
self.write().paint_stats = paint_stats.with_clipped_meshes(&clipped_meshes); self.write().paint_stats = paint_stats.with_clipped_meshes(&clipped_meshes);
clipped_meshes clipped_meshes
@ -961,8 +958,8 @@ impl Context {
.show(ui, |ui| { .show(ui, |ui| {
let mut font_definitions = self.fonts().definitions().clone(); let mut font_definitions = self.fonts().definitions().clone();
font_definitions.ui(ui); font_definitions.ui(ui);
let font_image = self.fonts().font_image(); let font_image_size = self.fonts().font_image_size();
font_image.ui(ui); crate::introspection::font_texture_ui(ui, font_image_size);
self.set_fonts(font_definitions); self.set_fonts(font_definitions);
}); });

View file

@ -1,13 +1,16 @@
//! uis for egui types. //! uis for egui types.
use crate::*; use crate::*;
impl Widget for &epaint::FontImage { // Show font texture in demo Ui
fn ui(self, ui: &mut Ui) -> Response { pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Response {
use epaint::Mesh; use epaint::Mesh;
ui.vertical(|ui| { ui.vertical(|ui| {
// Show font texture in demo Ui let color = if ui.visuals().dark_mode {
let [width, height] = self.size(); Color32::WHITE
} else {
Color32::BLACK
};
ui.label(format!( ui.label(format!(
"Texture size: {} x {} (hover to zoom)", "Texture size: {} x {} (hover to zoom)",
@ -22,11 +25,7 @@ impl Widget for &epaint::FontImage {
} }
let (rect, response) = ui.allocate_at_least(size, Sense::hover()); let (rect, response) = ui.allocate_at_least(size, Sense::hover());
let mut mesh = Mesh::default(); let mut mesh = Mesh::default();
mesh.add_rect_with_uv( mesh.add_rect_with_uv(rect, [pos2(0.0, 0.0), pos2(1.0, 1.0)].into(), color);
rect,
[pos2(0.0, 0.0), pos2(1.0, 1.0)].into(),
Color32::WHITE,
);
ui.painter().add(Shape::mesh(mesh)); 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);
@ -48,13 +47,12 @@ impl Widget for &epaint::FontImage {
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(); let mut mesh = Mesh::default();
mesh.add_rect_with_uv(zoom_rect, uv_rect, Color32::WHITE); mesh.add_rect_with_uv(zoom_rect, uv_rect, color);
ui.painter().add(Shape::mesh(mesh)); ui.painter().add(Shape::mesh(mesh));
} }
}); });
}) })
.response .response
}
} }
impl Widget for &mut epaint::text::FontDefinitions { impl Widget for &mut epaint::text::FontDefinitions {

View file

@ -1087,9 +1087,13 @@ pub struct PlotImage {
impl PlotImage { impl PlotImage {
/// Create a new image with position and size in plot coordinates. /// Create a new image with position and size in plot coordinates.
pub fn new(texture_id: impl Into<TextureId>, position: Value, size: impl Into<Vec2>) -> Self { pub fn new(
texture_id: impl Into<TextureId>,
center_position: Value,
size: impl Into<Vec2>,
) -> Self {
Self { Self {
position, position: center_position,
name: Default::default(), name: Default::default(),
highlight: false, highlight: false,
texture_id: texture_id.into(), texture_id: texture_id.into(),

View file

@ -114,11 +114,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let text_shape = TextShape::new(egui::Pos2::ZERO, galley); let text_shape = TextShape::new(egui::Pos2::ZERO, galley);
c.bench_function("tessellate_text", |b| { c.bench_function("tessellate_text", |b| {
b.iter(|| { b.iter(|| {
tessellator.tessellate_text( tessellator.tessellate_text(fonts.font_image_size(), text_shape.clone(), &mut mesh);
fonts.font_image().size(),
text_shape.clone(),
&mut mesh,
);
mesh.clear(); mesh.clear();
}) })
}); });

View file

@ -343,10 +343,7 @@ impl Widget for &mut ItemsDemo {
let image = PlotImage::new( let image = PlotImage::new(
texture, texture,
Value::new(0.0, 10.0), Value::new(0.0, 10.0),
[ 5.0 * vec2(texture.aspect_ratio(), 1.0),
ui.fonts().font_image().width() as f32 / 100.0,
ui.fonts().font_image().height() as f32 / 100.0,
],
); );
let plot = Plot::new("items_demo") let plot = Plot::new("items_demo")

View file

@ -70,8 +70,8 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
integration.update(display.gl_window().window()); integration.update(display.gl_window().window());
let clipped_meshes = integration.egui_ctx.tessellate(shapes); let clipped_meshes = integration.egui_ctx.tessellate(shapes);
for (id, image) in textures_delta.set { for (id, image_delta) in textures_delta.set {
painter.set_texture(&display, id, &image); painter.set_texture(&display, id, &image_delta);
} }
// paint: // paint:

View file

@ -155,8 +155,8 @@ impl EguiGlium {
let shapes = std::mem::take(&mut self.shapes); let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta); let mut textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set { for (id, image_delta) in textures_delta.set {
self.painter.set_texture(display, id, &image); self.painter.set_texture(display, id, &image_delta);
} }
let clipped_meshes = self.egui_ctx.tessellate(shapes); let clipped_meshes = self.egui_ctx.tessellate(shapes);

View file

@ -185,9 +185,9 @@ impl Painter {
&mut self, &mut self,
facade: &dyn glium::backend::Facade, facade: &dyn glium::backend::Facade,
tex_id: egui::TextureId, 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) => { egui::ImageData::Color(image) => {
assert_eq!( assert_eq!(
image.width() * image.height(), image.width() * image.height(),
@ -206,16 +206,30 @@ impl Painter {
}; };
let glium_image = glium::texture::RawImage2d { let glium_image = glium::texture::RawImage2d {
data: std::borrow::Cow::Owned(pixels), data: std::borrow::Cow::Owned(pixels),
width: image.width() as _, width: delta.image.width() as _,
height: image.height() as _, height: delta.image.height() as _,
format: glium::texture::ClientFormat::U8U8U8U8, format: glium::texture::ClientFormat::U8U8U8U8,
}; };
let format = texture::SrgbFormat::U8U8U8U8; let format = texture::SrgbFormat::U8U8U8U8;
let mipmaps = texture::MipmapsOption::NoMipmap; let mipmaps = texture::MipmapsOption::NoMipmap;
let gl_texture = SrgbTexture2d::with_format(facade, glium_image, format, mipmaps).unwrap();
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()); self.textures.insert(tex_id, gl_texture.into());
} }
}
pub fn free_texture(&mut self, tex_id: egui::TextureId) { pub fn free_texture(&mut self, tex_id: egui::TextureId) {
self.textures.remove(&tex_id); self.textures.remove(&tex_id);

View file

@ -86,8 +86,8 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
integration.update(gl_window.window()); integration.update(gl_window.window());
let clipped_meshes = integration.egui_ctx.tessellate(shapes); let clipped_meshes = integration.egui_ctx.tessellate(shapes);
for (id, image) in textures_delta.set { for (id, image_delta) in textures_delta.set {
painter.set_texture(&gl, id, &image); painter.set_texture(&gl, id, &image_delta);
} }
// paint: // paint:

View file

@ -175,8 +175,8 @@ impl EguiGlow {
let shapes = std::mem::take(&mut self.shapes); let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta); let mut textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set { for (id, image_delta) in textures_delta.set {
self.painter.set_texture(gl, id, &image); self.painter.set_texture(gl, id, &image_delta);
} }
let clipped_meshes = self.egui_ctx.tessellate(shapes); let clipped_meshes = self.egui_ctx.tessellate(shapes);

View file

@ -340,6 +340,7 @@ impl Painter {
gl.bind_texture(glow::TEXTURE_2D, Some(texture)); gl.bind_texture(glow::TEXTURE_2D, Some(texture));
} }
// Transform clip rect to physical pixels: // Transform clip rect to physical pixels:
let clip_min_x = pixels_per_point * clip_rect.min.x; let clip_min_x = pixels_per_point * clip_rect.min.x;
let clip_min_y = pixels_per_point * clip_rect.min.y; let clip_min_y = pixels_per_point * clip_rect.min.y;
@ -386,7 +387,7 @@ impl Painter {
&mut self, &mut self,
gl: &glow::Context, gl: &glow::Context,
tex_id: egui::TextureId, tex_id: egui::TextureId,
image: &egui::ImageData, delta: &egui::epaint::ImageDelta,
) { ) {
self.assert_not_destroyed(); self.assert_not_destroyed();
@ -398,7 +399,7 @@ impl Painter {
gl.bind_texture(glow::TEXTURE_2D, Some(glow_texture)); gl.bind_texture(glow::TEXTURE_2D, Some(glow_texture));
} }
match image { match &delta.image {
egui::ImageData::Color(image) => { egui::ImageData::Color(image) => {
assert_eq!( assert_eq!(
image.width() * image.height(), image.width() * image.height(),
@ -408,7 +409,7 @@ impl Painter {
let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref()); 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) => { egui::ImageData::Alpha(image) => {
assert_eq!( assert_eq!(
@ -427,12 +428,18 @@ impl Painter {
.flat_map(|a| a.to_array()) .flat_map(|a| a.to_array())
.collect(); .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_eq!(data.len(), w * h * 4);
assert!(w >= 1 && h >= 1); assert!(w >= 1 && h >= 1);
unsafe { unsafe {
@ -457,38 +464,50 @@ impl Painter {
glow::TEXTURE_WRAP_T, glow::TEXTURE_WRAP_T,
glow::CLAMP_TO_EDGE as i32, 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 { let format = if self.srgb_support {
glow::SRGB_ALPHA glow::SRGB_ALPHA
} else { } else {
glow::RGBA glow::RGBA
}; };
gl.tex_image_2d( (format, format)
glow::TEXTURE_2D,
0,
format as i32,
w as i32,
h as i32,
0,
format,
glow::UNSIGNED_BYTE,
Some(data),
);
} else { } 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( gl.tex_image_2d(
glow::TEXTURE_2D, glow::TEXTURE_2D,
0, level,
glow::SRGB8_ALPHA8 as i32, internal_format as _,
w as i32, w as _,
h as i32, h as _,
0, border,
glow::RGBA, src_format,
glow::UNSIGNED_BYTE, glow::UNSIGNED_BYTE,
Some(data), Some(data),
); );
check_for_gl_error(gl, "tex_image_2d");
} }
check_for_gl_error(gl, "upload_texture_srgb");
} }
} }

View file

@ -221,8 +221,8 @@ impl AppRunner {
/// Paint the results of the last call to [`Self::logic`]. /// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self, clipped_meshes: Vec<egui::ClippedMesh>) -> Result<(), JsValue> { pub fn paint(&mut self, clipped_meshes: Vec<egui::ClippedMesh>) -> Result<(), JsValue> {
let textures_delta = std::mem::take(&mut self.textures_delta); let textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set { for (id, image_delta) in textures_delta.set {
self.painter.set_texture(id, image); self.painter.set_texture(id, &image_delta);
} }
self.painter.clear(self.app.clear_color()); self.painter.clear(self.app.clear_color());

View file

@ -40,8 +40,8 @@ impl WrappedGlowPainter {
} }
impl crate::Painter for WrappedGlowPainter { impl crate::Painter for WrappedGlowPainter {
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) {
self.painter.set_texture(&self.glow_ctx, tex_id, &image); self.painter.set_texture(&self.glow_ctx, tex_id, delta);
} }
fn free_texture(&mut self, tex_id: egui::TextureId) { fn free_texture(&mut self, tex_id: egui::TextureId) {

View file

@ -1,7 +1,7 @@
use wasm_bindgen::prelude::JsValue; use wasm_bindgen::prelude::JsValue;
pub trait Painter { 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); fn free_texture(&mut self, tex_id: egui::TextureId);

View file

@ -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 srgb_supported = matches!(gl.get_extension("EXT_sRGB"), Ok(Some(_)));
let vert_shader = compile_shader( let vert_shader = compile_shader(
@ -222,36 +215,61 @@ impl WebGlPainter {
Ok(()) 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 = &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_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_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_MIN_FILTER, Gl::LINEAR as _);
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_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 level = 0;
let internal_format = self.texture_format; let internal_format = self.texture_format;
let border = 0; let border = 0;
let src_format = self.texture_format; let src_format = self.texture_format;
let src_type = Gl::UNSIGNED_BYTE; let src_type = Gl::UNSIGNED_BYTE;
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.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D, Gl::TEXTURE_2D,
level, level,
internal_format as _, internal_format as _,
size[0] as _, w as _,
size[1] as _, h as _,
border, border,
src_format, src_format,
src_type, src_type,
Some(pixels), Some(pixels),
) )
.unwrap(); .unwrap();
}
self.textures.insert(tex_id, gl_texture);
} }
} }
@ -271,8 +289,8 @@ impl epi::NativeTexture for WebGlPainter {
} }
impl crate::Painter for WebGlPainter { impl crate::Painter for WebGlPainter {
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) {
match image { match &delta.image {
egui::ImageData::Color(image) => { egui::ImageData::Color(image) => {
assert_eq!( assert_eq!(
image.width() * image.height(), image.width() * image.height(),
@ -281,7 +299,7 @@ impl crate::Painter for WebGlPainter {
); );
let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref()); 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) => { egui::ImageData::Alpha(image) => {
let gamma = if self.post_process.is_none() { let gamma = if self.post_process.is_none() {
@ -293,7 +311,7 @@ impl crate::Painter for WebGlPainter {
.srgba_pixels(gamma) .srgba_pixels(gamma)
.flat_map(|a| a.to_array()) .flat_map(|a| a.to_array())
.collect(); .collect();
self.set_texture_rgba(tex_id, image.size, &data); self.set_texture_rgba(tex_id, delta.pos, image.size, &data);
} }
}; };
} }

View file

@ -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( let vert_shader = compile_shader(
&gl, &gl,
Gl::VERTEX_SHADER, Gl::VERTEX_SHADER,
@ -206,37 +199,61 @@ impl WebGl2Painter {
Ok(()) 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 = &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_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_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_MIN_FILTER, Gl::LINEAR as _);
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_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 level = 0;
let internal_format = Gl::SRGB8_ALPHA8; let internal_format = Gl::SRGB8_ALPHA8;
let border = 0; let border = 0;
let src_format = Gl::RGBA; let src_format = Gl::RGBA;
let src_type = Gl::UNSIGNED_BYTE; let src_type = Gl::UNSIGNED_BYTE;
gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1); 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.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D, Gl::TEXTURE_2D,
level, level,
internal_format as i32, internal_format as _,
size[0] as i32, w as _,
size[1] as i32, h as _,
border, border,
src_format, src_format,
src_type, src_type,
Some(pixels), Some(pixels),
) )
.unwrap(); .unwrap();
}
self.textures.insert(tex_id, gl_texture);
} }
} }
@ -256,8 +273,8 @@ impl epi::NativeTexture for WebGl2Painter {
} }
impl crate::Painter for WebGl2Painter { impl crate::Painter for WebGl2Painter {
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) {
match image { match &delta.image {
egui::ImageData::Color(image) => { egui::ImageData::Color(image) => {
assert_eq!( assert_eq!(
image.width() * image.height(), image.width() * image.height(),
@ -266,7 +283,7 @@ impl crate::Painter for WebGl2Painter {
); );
let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref()); 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) => { egui::ImageData::Alpha(image) => {
let gamma = 1.0; let gamma = 1.0;
@ -274,7 +291,7 @@ impl crate::Painter for WebGl2Painter {
.srgba_pixels(gamma) .srgba_pixels(gamma)
.flat_map(|a| a.to_array()) .flat_map(|a| a.to_array())
.collect(); .collect();
self.set_texture_rgba(tex_id, image.size, &data); self.set_texture_rgba(tex_id, delta.pos, image.size, &data);
} }
}; };
} }

View file

@ -211,6 +211,23 @@ impl AlphaImage {
.iter() .iter()
.map(move |&a| srgba_from_alpha_lut[a as usize]) .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 { impl std::ops::Index<(usize, usize)> for AlphaImage {
@ -239,3 +256,45 @@ impl From<AlphaImage> for ImageData {
Self::Alpha(image) 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()
}
}

View file

@ -105,7 +105,7 @@ pub mod util;
pub use { pub use {
color::{Color32, Rgba}, color::{Color32, Rgba},
image::{AlphaImage, ColorImage, ImageData}, image::{AlphaImage, ColorImage, ImageData, ImageDelta},
mesh::{Mesh, Mesh16, Vertex}, mesh::{Mesh, Mesh16, Vertex},
shadow::Shadow, shadow::Shadow,
shape::{CircleShape, PathShape, RectShape, Shape, TextShape}, shape::{CircleShape, PathShape, RectShape, Shape, TextShape},
@ -113,7 +113,7 @@ pub use {
stroke::Stroke, stroke::Stroke,
tessellator::{tessellate_shapes, TessellationOptions, Tessellator}, tessellator::{tessellate_shapes, TessellationOptions, Tessellator},
text::{Fonts, Galley, TextStyle}, text::{Fonts, Galley, TextStyle},
texture_atlas::{FontImage, TextureAtlas}, texture_atlas::TextureAtlas,
texture_handle::TextureHandle, texture_handle::TextureHandle,
textures::TextureManager, textures::TextureManager,
}; };

View file

@ -374,14 +374,12 @@ fn allocate_glyph(
if glyph_width == 0 || glyph_height == 0 { if glyph_width == 0 || glyph_height == 0 {
UvRect::default() UvRect::default()
} else { } else {
let glyph_pos = atlas.allocate((glyph_width, glyph_height)); let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
let image = atlas.image_mut();
glyph.draw(|x, y, v| { glyph.draw(|x, y, v| {
if v > 0.0 { if v > 0.0 {
let px = glyph_pos.0 + x as usize; let px = glyph_pos.0 + x as usize;
let py = glyph_pos.1 + y 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;
} }
}); });

View file

@ -6,7 +6,7 @@ use crate::{
font::{Font, FontImpl}, font::{Font, FontImpl},
Galley, LayoutJob, Galley, LayoutJob,
}, },
FontImage, TextureAtlas, TextureAtlas,
}; };
// TODO: rename // TODO: rename
@ -233,11 +233,6 @@ pub struct Fonts {
definitions: FontDefinitions, definitions: FontDefinitions,
fonts: BTreeMap<TextStyle, Font>, fonts: BTreeMap<TextStyle, Font>,
atlas: Arc<Mutex<TextureAtlas>>, 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>, galley_cache: Mutex<GalleyCache>,
} }
@ -257,9 +252,9 @@ impl Fonts {
{ {
// Make the top left pixel fully white: // 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)); assert_eq!(pos, (0, 0));
atlas.image_mut().image[pos] = 255; image[pos] = 255;
} }
let atlas = Arc::new(Mutex::new(atlas)); let atlas = Arc::new(Mutex::new(atlas));
@ -283,19 +278,11 @@ impl Fonts {
}) })
.collect(); .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 { Self {
pixels_per_point, pixels_per_point,
definitions, definitions,
fonts, fonts,
atlas, atlas,
buffered_font_image: Default::default(),
galley_cache: Default::default(), galley_cache: Default::default(),
} }
} }
@ -319,15 +306,14 @@ impl Fonts {
(point * self.pixels_per_point).floor() / self.pixels_per_point (point * self.pixels_per_point).floor() / self.pixels_per_point
} }
/// Call each frame to get the latest available font texture data. /// Call each frame to get the change to the font texture since last call.
pub fn font_image(&self) -> Arc<FontImage> { pub fn font_image_delta(&self) -> Option<crate::ImageDelta> {
let atlas = self.atlas.lock(); self.atlas.lock().take_delta()
let mut buffered_texture = self.buffered_font_image.lock();
if buffered_texture.version != atlas.image().version {
*buffered_texture = Arc::new(atlas.image().clone());
} }
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. /// Width of this character in points.

View file

@ -1,39 +1,40 @@
use crate::image::AlphaImage; use crate::{AlphaImage, ImageDelta};
/// An 8-bit texture containing font data. #[derive(Clone, Copy, Eq, PartialEq)]
#[derive(Clone, Default)] struct Rectu {
pub struct FontImage { /// inclusive
/// e.g. a hash of the data. Use this to detect changes! min_x: usize,
/// If the texture changes, this too will change. /// inclusive
pub version: u64, min_y: usize,
/// exclusive
/// The actual image data. max_x: usize,
pub image: AlphaImage, /// exclusive
max_y: usize,
} }
impl FontImage { impl Rectu {
#[inline] const NOTHING: Self = Self {
pub fn size(&self) -> [usize; 2] { min_x: usize::MAX,
self.image.size min_y: usize::MAX,
} max_x: 0,
max_y: 0,
#[inline] };
pub fn width(&self) -> usize { const EVERYTHING: Self = Self {
self.image.size[0] min_x: 0,
} min_y: 0,
max_x: usize::MAX,
#[inline] max_y: usize::MAX,
pub fn height(&self) -> usize { };
self.image.size[1]
}
} }
/// Contains font data in an atlas, where each character occupied a small rectangle. /// Contains font data in an atlas, where each character occupied a small rectangle.
/// ///
/// More characters can be added, possibly expanding the texture. /// More characters can be added, possibly expanding the texture.
#[derive(Clone, Default)] #[derive(Clone)]
pub struct TextureAtlas { pub struct TextureAtlas {
image: FontImage, image: AlphaImage,
/// What part of the image that is dirty
dirty: Rectu,
/// Used for when allocating new rectangles. /// Used for when allocating new rectangles.
cursor: (usize, usize), cursor: (usize, usize),
@ -43,25 +44,35 @@ pub struct TextureAtlas {
impl TextureAtlas { impl TextureAtlas {
pub fn new(size: [usize; 2]) -> Self { pub fn new(size: [usize; 2]) -> Self {
Self { Self {
image: FontImage {
version: 0,
image: AlphaImage::new(size), image: AlphaImage::new(size),
}, dirty: Rectu::EVERYTHING,
..Default::default() cursor: (0, 0),
row_height: 0,
} }
} }
pub fn image(&self) -> &FontImage { pub fn size(&self) -> [usize; 2] {
&self.image self.image.size
} }
pub fn image_mut(&mut self) -> &mut FontImage { /// Call to get the change to the image since last call.
self.image.version += 1; pub fn take_delta(&mut self) -> Option<ImageDelta> {
&mut self.image 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. /// Returns the coordinates of where the rect ended up,
pub fn allocate(&mut self, (w, h): (usize, usize)) -> (usize, usize) { /// 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 /// On some low-precision GPUs (my old iPad) characters get muddled up
/// if we don't add some empty pixels between the characters. /// if we don't add some empty pixels between the characters.
/// On modern high-precision GPUs this is not needed. /// On modern high-precision GPUs this is not needed.
@ -81,21 +92,31 @@ impl TextureAtlas {
} }
self.row_height = self.row_height.max(h); 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; let pos = self.cursor;
self.cursor.0 += w + PADDING; 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() { while min_height >= image.height() {
image.size[1] *= 2; // double the height image.size[1] *= 2; // double the height
} }
if image.width() * image.height() > image.pixels.len() { if image.width() * image.height() > image.pixels.len() {
image.pixels.resize(image.width() * image.height(), 0); image.pixels.resize(image.width() * image.height(), 0);
true
} else {
false
} }
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
emath::NumExt, emath::NumExt,
mutex::{Arc, RwLock}, mutex::{Arc, RwLock},
ImageData, TextureId, TextureManager, ImageData, ImageDelta, TextureId, TextureManager,
}; };
/// Used to paint images. /// Used to paint images.
@ -66,7 +66,16 @@ impl TextureHandle {
/// Assign a new image to an existing texture. /// Assign a new image to an existing texture.
pub fn set(&mut self, image: impl Into<ImageData>) { 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 /// width x height

View file

@ -1,4 +1,4 @@
use crate::{image::ImageData, TextureId}; use crate::{ImageData, ImageDelta, TextureId};
use ahash::AHashMap; use ahash::AHashMap;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -38,16 +38,19 @@ impl TextureManager {
retain_count: 1, retain_count: 1,
}); });
self.delta.set.insert(id, image); self.delta.set.insert(id, ImageDelta::full(image));
id id
} }
/// Assign a new image to an existing texture. /// Assign a new image to an existing texture,
pub fn set(&mut self, id: TextureId, image: ImageData) { /// 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(meta) = self.metas.get_mut(&id) {
meta.size = image.size(); if delta.is_whole() {
meta.bytes_per_pixel = image.bytes_per_pixel(); meta.size = delta.image.size();
self.delta.set.insert(id, image); meta.bytes_per_pixel = delta.image.bytes_per_pixel();
}
self.delta.set.insert(id, delta);
} else { } else {
crate::epaint_assert!( crate::epaint_assert!(
false, false,
@ -147,7 +150,7 @@ impl TextureMeta {
#[must_use = "The painter must take care of this"] #[must_use = "The painter must take care of this"]
pub struct TexturesDelta { pub struct TexturesDelta {
/// New or changed textures. Apply before painting. /// New or changed textures. Apply before painting.
pub set: AHashMap<TextureId, ImageData>, pub set: AHashMap<TextureId, ImageDelta>,
/// Texture to free after painting. /// Texture to free after painting.
pub free: Vec<TextureId>, pub free: Vec<TextureId>,