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)).
* 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))

View file

@ -33,7 +33,6 @@ struct ContextImpl {
fonts: Option<Fonts>,
memory: Memory,
animation_manager: AnimationManager,
latest_font_image_version: Option<u64>,
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);
});

View file

@ -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 {

View file

@ -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<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 {
position,
position: center_position,
name: Default::default(),
highlight: false,
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);
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();
})
});

View file

@ -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")

View file

@ -70,8 +70,8 @@ pub fn run(app: Box<dyn epi::App>, 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:

View file

@ -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);

View file

@ -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) {

View file

@ -86,8 +86,8 @@ pub fn run(app: Box<dyn epi::App>, 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:

View file

@ -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);

View file

@ -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");
}
}

View file

@ -221,8 +221,8 @@ impl AppRunner {
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self, clipped_meshes: Vec<egui::ClippedMesh>) -> 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());

View file

@ -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) {

View file

@ -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);

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 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);
}
};
}

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(
&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);
}
};
}

View file

@ -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()
}
}

View file

@ -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,
};

View file

@ -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;
}
});

View file

@ -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.

View file

@ -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
}
}

View file

@ -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

View file

@ -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>,