diff --git a/CHANGELOG.md b/CHANGELOG.md index 440fa8fc..b5c892fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w ## Unreleased -* Add `*_released` & `*_clicked` methods for `PointerState`. +* Add `*_released` & `*_clicked` methods for `PointerState` ([#1582](https://github.com/emilk/egui/pull/1582)). +* Optimize painting of filled circles (e.g. for scatter plots) by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)). + ## 0.18.1 - 2022-05-01 * Change `Shape::Callback` from `&dyn Any` to `&mut dyn Any` to support more backends. diff --git a/egui/src/context.rs b/egui/src/context.rs index 63e1a240..38cdbcab 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -830,14 +830,17 @@ impl Context { let pixels_per_point = self.pixels_per_point(); let tessellation_options = *self.tessellation_options(); - let font_image_size = self.fonts().font_image_size(); + let texture_atlas = self.fonts().texture_atlas(); + let font_tex_size = texture_atlas.lock().size(); + let prepared_discs = texture_atlas.lock().prepared_discs(); let paint_stats = PaintStats::from_shapes(&shapes); let clipped_primitives = tessellator::tessellate_shapes( pixels_per_point, tessellation_options, + font_tex_size, + prepared_discs, shapes, - font_image_size, ); self.write().paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives); clipped_primitives diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 91a1a8a4..c0003695 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -142,6 +142,7 @@ impl Widget for &mut epaint::TessellationOptions { feathering, feathering_size_in_pixels, coarse_tessellation_culling, + prerasterized_discs, round_text_to_pixels, debug_paint_clip_rects, debug_paint_text_rects, @@ -158,6 +159,8 @@ impl Widget for &mut epaint::TessellationOptions { .text("Feathering size in pixels"); ui.add_enabled(*feathering, feathering_slider); + ui.checkbox(prerasterized_discs, "Speed up filled circles with pre-rasterization"); + ui.add( crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0) .logarithmic(true) diff --git a/egui_demo_lib/benches/benchmark.rs b/egui_demo_lib/benches/benchmark.rs index 0dceba6a..9e9f52ad 100644 --- a/egui_demo_lib/benches/benchmark.rs +++ b/egui_demo_lib/benches/benchmark.rs @@ -124,8 +124,13 @@ pub fn criterion_benchmark(c: &mut Criterion) { let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width); let font_image_size = fonts.font_image_size(); - let mut tessellator = - egui::epaint::Tessellator::new(1.0, Default::default(), font_image_size); + let prepared_discs = fonts.texture_atlas().lock().prepared_discs(); + let mut tessellator = egui::epaint::Tessellator::new( + 1.0, + Default::default(), + font_image_size, + prepared_discs, + ); let mut mesh = egui::epaint::Mesh::default(); let text_shape = TextShape::new(egui::Pos2::ZERO, galley); c.bench_function("tessellate_text", |b| { diff --git a/egui_demo_lib/src/demo/misc_demo_window.rs b/egui_demo_lib/src/demo/misc_demo_window.rs index 85262e54..b379239c 100644 --- a/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/egui_demo_lib/src/demo/misc_demo_window.rs @@ -158,6 +158,20 @@ impl View for MiscDemoWindow { painter.line_segment([c, c + r * Vec2::angled(TAU * 3.0 / 8.0)], stroke); }); }); + + CollapsingHeader::new("Many circles of different sizes") + .default_open(false) + .show(ui, |ui| { + ui.horizontal_wrapped(|ui| { + for i in 0..100 { + let r = i as f32 * 0.5; + let size = Vec2::splat(2.0 * r + 5.0); + let (rect, _response) = ui.allocate_at_least(size, Sense::hover()); + ui.painter() + .circle_filled(rect.center(), r, ui.visuals().text_color()); + } + }); + }); } } diff --git a/epaint/CHANGELOG.md b/epaint/CHANGELOG.md index e0a42144..9612e77b 100644 --- a/epaint/CHANGELOG.md +++ b/epaint/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to the epaint crate will be documented in this file. ## Unreleased +* Optimize tessellation of filled circles by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)). ## 0.18.1 - 2022-05-01 diff --git a/epaint/benches/benchmark.rs b/epaint/benches/benchmark.rs index 3ae5d901..126ec2bf 100644 --- a/epaint/benches/benchmark.rs +++ b/epaint/benches/benchmark.rs @@ -1,6 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use epaint::{pos2, Color32, Shape, Stroke}; +use epaint::*; fn single_dashed_lines(c: &mut Criterion) { c.bench_function("single_dashed_lines", move |b| { @@ -39,5 +39,43 @@ fn many_dashed_lines(c: &mut Criterion) { }); } -criterion_group!(benches, single_dashed_lines, many_dashed_lines); +fn tessellate_circles(c: &mut Criterion) { + c.bench_function("tessellate_circles_100k", move |b| { + let radii: [f32; 10] = [1.0, 2.0, 3.6, 4.0, 5.7, 8.0, 10.0, 13.0, 15.0, 17.0]; + let mut clipped_shapes = vec![]; + for r in radii { + for _ in 0..10_000 { + let clip_rect = Rect::from_min_size(Pos2::ZERO, Vec2::splat(1024.0)); + let shape = Shape::circle_filled(Pos2::new(10.0, 10.0), r, Color32::WHITE); + clipped_shapes.push(ClippedShape(clip_rect, shape)); + } + } + assert_eq!(clipped_shapes.len(), 100_000); + + let pixels_per_point = 2.0; + let options = TessellationOptions::default(); + + let atlas = TextureAtlas::new([4096, 256]); + let font_tex_size = atlas.size(); + let prepared_discs = atlas.prepared_discs(); + + b.iter(|| { + let clipped_primitive = tessellate_shapes( + pixels_per_point, + options, + font_tex_size, + prepared_discs.clone(), + clipped_shapes.clone(), + ); + black_box(clipped_primitive); + }); + }); +} + +criterion_group!( + benches, + single_dashed_lines, + many_dashed_lines, + tessellate_circles +); criterion_main!(benches); diff --git a/epaint/src/shadow.rs b/epaint/src/shadow.rs index c6590aca..efbb2e50 100644 --- a/epaint/src/shadow.rs +++ b/epaint/src/shadow.rs @@ -73,6 +73,7 @@ impl Shadow { ..Default::default() }, font_tex_size, + vec![], ); let mut mesh = Mesh::default(); tessellator.tessellate_rect(&rect, &mut mesh); diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index f6e84a5d..a3e9d8a7 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -5,6 +5,7 @@ #![allow(clippy::identity_op)] +use crate::texture_atlas::PreparedDisc; use crate::*; use emath::*; @@ -618,6 +619,10 @@ pub struct TessellationOptions { /// This likely makes pub coarse_tessellation_culling: bool, + /// If `true`, small filled circled will be optimized by using pre-rasterized circled + /// from the font atlas. + pub prerasterized_discs: bool, + /// If `true` (default) align text to mesh grid. /// This makes the text sharper on most platforms. pub round_text_to_pixels: bool, @@ -644,6 +649,7 @@ impl Default for TessellationOptions { feathering: true, feathering_size_in_pixels: 1.0, coarse_tessellation_culling: true, + prerasterized_discs: true, round_text_to_pixels: true, debug_paint_text_rects: false, debug_paint_clip_rects: false, @@ -968,6 +974,8 @@ pub struct Tessellator { pixels_per_point: f32, options: TessellationOptions, font_tex_size: [usize; 2], + /// See [`TextureAtlas::prepared_discs`]. + prepared_discs: Vec, /// size of feathering in points. normally the size of a physical pixel. 0.0 if disabled feathering: f32, /// Only used for culling @@ -980,10 +988,12 @@ impl Tessellator { /// Create a new [`Tessellator`]. /// /// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text. + /// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec. pub fn new( pixels_per_point: f32, options: TessellationOptions, font_tex_size: [usize; 2], + prepared_discs: Vec, ) -> Self { let feathering = if options.feathering { let pixel_size = 1.0 / pixels_per_point; @@ -995,6 +1005,7 @@ impl Tessellator { pixels_per_point, options, font_tex_size, + prepared_discs, feathering, clip_rect: Rect::EVERYTHING, scratchpad_points: Default::default(), @@ -1137,7 +1148,7 @@ impl Tessellator { let CircleShape { center, radius, - fill, + mut fill, stroke, } = shape; @@ -1154,6 +1165,30 @@ impl Tessellator { return; } + if self.options.prerasterized_discs && fill != Color32::TRANSPARENT { + let radius_px = radius * self.pixels_per_point; + // strike the right balance between some circles becoming too blurry, and some too sharp. + let cutoff_radius = radius_px * 2.0_f32.powf(0.25); + + // Find the right disc radius for a crisp edge: + // TODO: perhaps we can do something faster than this linear search. + for disc in &self.prepared_discs { + if cutoff_radius <= disc.r { + let side = radius_px * disc.w / (self.pixels_per_point * disc.r); + let rect = Rect::from_center_size(center, Vec2::splat(side)); + out.add_rect_with_uv(rect, disc.uv, fill); + + if stroke.is_empty() { + return; // we are done + } else { + // we still need to do the stroke + fill = Color32::TRANSPARENT; // don't fill again below + break; + } + } + } + } + self.scratchpad_path.clear(); self.scratchpad_path.add_circle(center, radius); self.scratchpad_path.fill(self.feathering, fill, out); @@ -1476,7 +1511,8 @@ impl Tessellator { /// * `pixels_per_point`: number of physical pixels to each logical point /// * `options`: tessellation quality /// * `shapes`: what to tessellate -/// * `font_tex_size`: size of the font texture (required to normalize glyph uv rectangles) +/// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text. +/// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec. /// /// The implementation uses a [`Tessellator`]. /// @@ -1485,10 +1521,12 @@ impl Tessellator { pub fn tessellate_shapes( pixels_per_point: f32, options: TessellationOptions, - shapes: Vec, font_tex_size: [usize; 2], + prepared_discs: Vec, + shapes: Vec, ) -> Vec { - let mut tessellator = Tessellator::new(pixels_per_point, options, font_tex_size); + let mut tessellator = + Tessellator::new(pixels_per_point, options, font_tex_size, prepared_discs); let mut clipped_primitives: Vec = Vec::default(); @@ -1562,6 +1600,15 @@ fn test_tessellator() { let shape = Shape::Vec(shapes); let clipped_shapes = vec![ClippedShape(rect, shape)]; - let primitives = tessellate_shapes(1.0, Default::default(), clipped_shapes, [100, 100]); + let font_tex_size = [1024, 1024]; // unused + let prepared_discs = vec![]; // unused + + let primitives = tessellate_shapes( + 1.0, + Default::default(), + font_tex_size, + prepared_discs, + clipped_shapes, + ); assert_eq!(primitives.len(), 2); } diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index d699e101..611a4931 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -394,6 +394,12 @@ impl Fonts { self.lock().fonts.max_texture_side } + /// The font atlas. + /// Pass this to [`crate::Tessellator`]. + pub fn texture_atlas(&self) -> Arc> { + self.lock().fonts.atlas.clone() + } + /// Current size of the font image. /// Pass this to [`crate::Tessellator`]. pub fn font_image_size(&self) -> [usize; 2] { @@ -535,14 +541,7 @@ impl FontsImpl { let texture_width = max_texture_side.at_most(8 * 1024); let initial_height = 64; - let mut atlas = TextureAtlas::new([texture_width, initial_height]); - - { - // Make the top left pixel fully white: - let (pos, image) = atlas.allocate((1, 1)); - assert_eq!(pos, (0, 0)); - image[pos] = 1.0; - } + let atlas = TextureAtlas::new([texture_width, initial_height]); let atlas = Arc::new(Mutex::new(atlas)); diff --git a/epaint/src/text/text_layout_types.rs b/epaint/src/text/text_layout_types.rs index 3af4069d..a0d04566 100644 --- a/epaint/src/text/text_layout_types.rs +++ b/epaint/src/text/text_layout_types.rs @@ -401,7 +401,7 @@ pub struct Glyph { pub pos: Pos2, /// Advance width and font row height. pub size: Vec2, - /// Position of the glyph in the font texture. + /// Position of the glyph in the font texture, in texels. pub uv_rect: UvRect, /// Index into [`LayoutJob::sections`]. Decides color etc. pub section_index: u32, diff --git a/epaint/src/texture_atlas.rs b/epaint/src/texture_atlas.rs index 0b8fe4dd..96d1f8b3 100644 --- a/epaint/src/texture_atlas.rs +++ b/epaint/src/texture_atlas.rs @@ -1,6 +1,8 @@ +use emath::{remap_clamp, Rect}; + use crate::{FontImage, ImageDelta}; -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Rectu { /// inclusive min_x: usize, @@ -27,6 +29,26 @@ impl Rectu { }; } +#[derive(Copy, Clone, Debug)] +struct PrerasterizedDisc { + r: f32, + uv: Rectu, +} + +/// A pre-rasterized disc (filled circle), somewhere in the texture atlas. +#[derive(Copy, Clone, Debug)] +pub struct PreparedDisc { + /// The radius of this disc in texels. + pub r: f32, + + /// Width in texels. + pub w: f32, + + /// Where in the texture atlas the disc is. + /// Normalized in 0-1 range. + pub uv: Rect, +} + /// Contains font data in an atlas, where each character occupied a small rectangle. /// /// More characters can be added, possibly expanding the texture. @@ -42,24 +64,98 @@ pub struct TextureAtlas { /// Set when someone requested more space than was available. overflowed: bool, + + /// pre-rasterized discs of radii `2^i`, where `i` is the index. + discs: Vec, } impl TextureAtlas { pub fn new(size: [usize; 2]) -> Self { assert!(size[0] >= 1024, "Tiny texture atlas"); - Self { + let mut atlas = Self { image: FontImage::new(size), dirty: Rectu::EVERYTHING, cursor: (0, 0), row_height: 0, overflowed: false, + discs: vec![], // will be filled in below + }; + + // Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color: + let (pos, image) = atlas.allocate((1, 1)); + assert_eq!(pos, (0, 0)); + image[pos] = 1.0; + + // Allocate a series of anti-aliased discs used to render small filled circles: + // TODO: these circles can be packed A LOT better. + // In fact, the whole texture atlas could be packed a lot better. + // for r in [1, 2, 4, 8, 16, 32, 64] { + // let w = 2 * r + 3; + // let hw = w as i32 / 2; + const LARGEST_CIRCLE_RADIUS: f32 = 64.0; + for i in 0.. { + let r = 2.0_f32.powf(i as f32 / 2.0 - 1.0); + if r > LARGEST_CIRCLE_RADIUS { + break; + } + let hw = (r + 0.5).ceil() as i32; + let w = (2 * hw + 1) as usize; + let ((x, y), image) = atlas.allocate((w, w)); + for dx in -hw..=hw { + for dy in -hw..=hw { + let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt(); + let coverage = remap_clamp( + distance_to_center, + (r as f32 - 0.5)..=(r as f32 + 0.5), + 1.0..=0.0, + ); + image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] = + coverage; + } + } + atlas.discs.push(PrerasterizedDisc { + r, + uv: Rectu { + min_x: x, + min_y: y, + max_x: x + w, + max_y: y + w, + }, + }); } + + atlas } pub fn size(&self) -> [usize; 2] { self.image.size } + /// Returns the locations and sizes of pre-rasterized discs (filled circles) in this atlas. + pub fn prepared_discs(&self) -> Vec { + let size = self.size(); + let inv_w = 1.0 / size[0] as f32; + let inv_h = 1.0 / size[1] as f32; + self.discs + .iter() + .map(|disc| { + let r = disc.r; + let Rectu { + min_x, + min_y, + max_x, + max_y, + } = disc.uv; + let w = max_x - min_x; + let uv = Rect::from_min_max( + emath::pos2(min_x as f32 * inv_w, min_y as f32 * inv_h), + emath::pos2(max_x as f32 * inv_w, max_y as f32 * inv_h), + ); + PreparedDisc { r, w: w as f32, uv } + }) + .collect() + } + fn max_height(&self) -> usize { // the initial width is likely the max texture side size self.image.width()